oapi-codegen icon indicating copy to clipboard operation
oapi-codegen copied to clipboard

Echo strict handler unexpected behavior filling request body with path params

Open Kurt212 opened this issue 1 year ago • 1 comments

Hello!

I encountered a problem while using code generation with the Echo framework and the strict handler functionality.

Problem Definition

The issue arises when defining an HTTP handler with path parameters and a JSON request body, where the request body object has no explicitly defined properties, but has additionalProperties: true. For example:

  /{param1}/{param2}:
    post:
      operationId: HandlerID
      parameters:
          - name: param1
            in: path
            required: true
            schema:
              type: string
          - name: param2
            in: path
            required: true
            schema:
              type: string
      requestBody:
          required: true
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

The code-generated request struct for the strict handler looks like this:


// HandlerIDJSONBody defines parameters for HandlerID.
type HandlerIDJSONBody map[string]interface{}

// HandlerIDJSONRequestBody defines body for HandlerID for application/json ContentType.
type HandlerIDJSONRequestBody HandlerIDJSONBody

type HandlerIDRequestObject struct {
	Param1 string `json:"param1"`
	Param2 string `json:"param2"`
	Body   *HandlerIDJSONRequestBody
}

The Body field may contain not only data from the request body but also path and query parameters.

The problem

The problem lies in the call to ctx.Bind on the Echo context object here: https://github.com/oapi-codegen/oapi-codegen/blob/2be24b11ae0f0a6610cffc0f1c1b43ea83fd82aa/pkg/codegen/templates/strict/strict-echo.tmpl#L35-L38

The Bind method not only unmarshals the body but also adds path and query parameters to the object, as stated in the Echo documentation:

	// Bind binds path params, query params and the request body into provided type `i`. The default binder
	// binds body based on Content-Type header.
	Bind(i interface{}) [error](https://pkg.go.dev/builtin#error)

As a result, the Body field is populated with path parameters as well.

Suggestion

I suggest replacing the ctx.Bind() call with DefaultBinder.BindBody(). Documentation reference: https://pkg.go.dev/github.com/labstack/echo/v4#DefaultBinder.BindBody.

// BindBody binds request body contents to bindable object
// NB: then binding forms take note that this implementation uses standard library form parsing
// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm
// See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm
// See MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm
func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {

Minimal reproduction

go version

go version go1.22.6 darwin/arm64

go.mod

require (
	github.com/labstack/echo/v4 v4.12.0
	github.com/oapi-codegen/oapi-codegen/v2 v2.3.0
	github.com/oapi-codegen/runtime v1.1.1
)

api.yaml

openapi: 3.0.1
info:
  license:
    name: Proprietary
    url: 'https://en.wikipedia.org/wiki/Proprietary_software'
  version: 0.1.0
  title: Test API
paths:
  /{param1}/{param2}:
    post:
      operationId: HandlerID
      parameters:
          - name: param1
            in: path
            required: true
            schema:
              type: string
          - name: param2
            in: path
            required: true
            schema:
              type: string
      requestBody:
          required: true
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
      responses:
        '200':
          description: OK

config.yaml

package: main
generate:
  echo-server: true
  strict-server: true
  models: true
output: ./cmd/main/api_server.gen.go

main.go for running and printing incoming Body

package main

import (
	"context"
	"fmt"
	"github.com/labstack/echo/v4"
	"net/http"
	"time"
)

type API struct{}

func (a *API) HandlerID(ctx context.Context, request HandlerIDRequestObject) (HandlerIDResponseObject, error) {
	fmt.Println(request.Param1, request.Param2)
	fmt.Println(request.Body)

	return HandlerID200Response{}, nil
}

func main() {
	echo := echo.New()

	strictHandler := NewStrictHandler(new(API), nil)

	RegisterHandlers(echo, strictHandler)

	server := &http.Server{ //nolint:exhaustruct
		Handler:           echo,
		Addr:              "localhost:8080",
		ReadHeaderTimeout: 1 * time.Second,
	}

	server.ListenAndServe()
}

Run this code and call curl in terminal

curl "localhost:8080/param1val/param2val" -X POST -H "Content-Type: application/json" -d '{"key": "val"}'

And the server will print following:

param1val param2val
&map[key:val param1:[param1val] param2:[param2val]]

What means that Body contains keys param1 and param2

Kurt212 avatar Sep 12 '24 17:09 Kurt212

I have this same problem unfortunately

SeaRoll avatar Aug 21 '25 06:08 SeaRoll