wI2L / fizz

:lemon: Gin wrapper with OpenAPI 3 spec generation
https://pkg.go.dev/github.com/wI2L/fizz
MIT License
214 stars 52 forks source link

[binding error] Parsing audio file sent in the request body together with query params #111

Open norbertstrzelecki opened 7 months ago

norbertstrzelecki commented 7 months ago

While sending an audio file within the request body, I'm receiving the following error:

{
    "error": "binding error: error parsing request body: invalid character 'R' looking for beginning of value"
}

As I understand, the audio file is parsed as a string and not the binary file -- the "R" is from string "RIFF" file header. The question is whether I could bind it as an audio/wav type as described below?

The request looks like this:

curl --location 'http://0.0.0.0:8080/test?userId=some-id' \
--header 'accept: application/json' \
--header 'Content-Type: audio/wav' \
--data '@/audio_file.wav'

So I send userId as a param and binary audio file in the body as audio/wav content type.

Code-wise, the handler functions look like this:

// Bind returns router parameters
func (h *Handler) Bind(router *fizz.Fizz) {
    router.POST("/test", []fizz.OperationOption{
        fizz.ID("test"),
    }, tonic.Handler(h.handle, http.StatusOK))
}

func (h *Handler) handle(c *gin.Context, _ *Evaluation) (*Response, error) {
    var request Request
    err := c.ShouldBindQuery(&request)
        /.../
    audio, err := io.ReadAll(c.Request.Body)
       /.../

    input := Evaluation{
        UserID:          request.UserID,
        Audio:            audio,
    }
    eval := h.Evaluate(c, input)
        /.../

    return &Response{Eval: eval}, nil

and the structs:

type Evaluation struct {
    UserID    string `query:"userId" validate:"required" description:"Id of user"`
    Audio     []byte `format:"binary"`
}

type Request struct {
    UserID    string    `query:"userId" validate:"required" description:"Id of user"`
}

type Response struct {
    Eval    string    `json:"eval"`
}

BTW While generating OpenAPI YAML, the part I'm interested in looks like this:

      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestInput'

components:
  schemas:
    TestInput:
      type: object
      properties:
        Audio:
          type: string
          format: binary

and I need this:

      requestBody:
        content:
          audio/wav:
            schema:
              type: string
              format: binary
jonas0616 commented 7 months ago

I've run into that too. Here's a workaround until fizz and tonic get updated. So, the binding error happens because tonic tries to use JSON unmarshal when it sees a handler with a request object. To get around it, you gotta set up a custom binding hook:

type Evaluation struct {
    UserID    string `query:"userId" validate:"required" description:"Id of user"`
}

// implement Binder for your request struct
func (e *Evaluation) ShouldBind() bool {
    return false
}

type Binder interface {
    ShouldBind() bool
}

func CustomBindHook(c *gin.Context, in any) error {
    if binder, ok := in.(Binder); ok {
        if !binder.ShouldBind() {
            return nil
        }
    }
    return tonic.DefaultBindingHook(c, in)
}

// add following code somewhere when setup route
tonic.SetBindHook(CustomBindHook)

Next, since you cannot set custom content types in fizz right now, you'll need to tweak the OpenAPI schema object directly. Because it's a bit of a hassle, I won't be writing the code here.