Unable to validate request body with strict server
When utilizing the strict server implementation, I am unable to apply a middleware that validates the request body against the spec. I quite like the strict server implementation as the workflow is very similar to my grpc services.
When I started off generating a non-strict server, I was able to apply the OapiValidateRequest middleware that took care of checking the request against the spec, however when I switched to strict I deduced that I am unable to read the body in the downstream strict middlewares. The culprit here is the generated strictHandler which does not replace r.Body after it decodes it to the message type which is unexpected since the strict server documentation says "... It can access to both request and response structs, as well as raw request\response data. ..."
For context, I have provided the function below that was generated to handle a POST /bin/lookup request. Reading through it, you can see that the body is read in and decoded to JSON immediately while r.Body is never replaced such that a downstream middleware could read it again and validate.
The main driver here is that since the field is required in my spec, the generated code is not a pointer, which when you JSON unmarshal the value will be empty with or without a value provided; this same issue occurs in GRPC and is the reason for a field mask there.
Ultimately, I am unable to discern between the following two requests with just the strict middleware: {} vs {"pan": ""} which means I am unable respond with the appropriate error; either required or invalid.
Technically I can make this work by utilizing a middleware on the Chi router, however I would prefer to utilize the strict middleware since I would like to return an error and then map that error to the correct response in the ResponseErrorHandlerFunc instead of utilizing an out-of-band middleware.
I would be fine making this change and submitting a PR if it is desired.
Spec:
LookupReq:
type: object
required: [pan]
properties:
pan:
type: string
Generated strictHandler:
// PostBinLookup operation middleware
func (sh *strictHandler) PostBinLookup(w http.ResponseWriter, r *http.Request) {
var request PostBinLookupRequestObject
var body PostBinLookupJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.PostBinLookup(ctx, request.(PostBinLookupRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "PostBinLookup")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(PostBinLookupResponseObject); ok {
if err := validResponse.VisitPostBinLookupResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("Unexpected response type: %T", response))
}
}
The primary change here could involve a simple TeeReader and byte buffer: (or read the body and keep it on the RequestObject struct that is passed around)
// PostBinLookup operation middleware
func (sh *strictHandler) PostBinLookup(w http.ResponseWriter, r *http.Request) {
var (
request PostBinLookupRequestObject
body PostBinLookupJSONRequestBody
b = bytes.NewBuffer(nil)
tr = io.TeeReader(r.Body, b)
err = json.NewDecoder(tr).Decode(&body)
)
if err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
...