From 4fb8536d01d2ca15d8508193ca07c874cb22a2dc Mon Sep 17 00:00:00 2001 From: David Newgas Date: Mon, 4 Aug 2025 15:36:28 -0700 Subject: [PATCH 1/5] LP-520: Don't parse body when x-raw-request:true x-raw-request currently passes the http.Request through to the handler as-is. This works well when there is no body defined in the openapi spec, which is how it has mostly been used to date. However when a body is defined in the openapi spec the body is parsed as per the spec. Beause r.Body() can only be read one time, this makes the raw request useless for any purposes that would access this body. This commit brings x-raw-request in line with the intended behaviour of leaving request processing to the handler by omitting the body parsing. This is a BREAKING CHANGE for any code that uses x-raw-request:true and a defined body, as the signature and responsibility of the user code changes. --- foji/openapi/handler.go.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foji/openapi/handler.go.tpl b/foji/openapi/handler.go.tpl index 9126ecf..2c461e4 100644 --- a/foji/openapi/handler.go.tpl +++ b/foji/openapi/handler.go.tpl @@ -12,7 +12,7 @@ {{ goToken (camel $param.Value.Name) -}} {{- if $.ParamIsOptionalType $param }} *{{ end }} {{ $.GetType $package $typeName $param.Value.Schema }}, {{- end -}} - {{- if isNotNil $body}} + {{- if and (not (empty $body)) (not ($.OpHasExtension $op "x-raw-request" )) }} {{- $type := $.GetType $package (print $op.OperationID " Request") $body.Schema }} body {{ $type -}} {{- end -}} ) ( @@ -356,7 +356,7 @@ func (h OpenAPIHandlers) {{ pascal $op.OperationID}}(w http.ResponseWriter, r *h } {{- end}} - {{- $hasBody := not (empty $opBody)}} + {{- $hasBody := and (not (empty $opBody)) (not ($.OpHasExtension $op "x-raw-request" )) }} {{- if $hasBody }} {{- $bodyType := $.GetType $package (print $op.OperationID " Request") $opBody.Schema}} {{- if $opBody.IsJson }} From bdbfe59135848a7c265a1a793398d3fcb85325a6 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 8 Aug 2025 13:22:14 -0700 Subject: [PATCH 2/5] LP-520: separate x-raw-body extension Per https://lavajira.atlassian.net/browse/LP-520?focusedCommentId=55950 it was by design for x-raw-request to preserve body parsing. This request thus makes a separate x-raw-body extension that prevents body parsing. In order to allow x-raw-body without x-raw-request, the body io.ReadCloser is now passed in as a parameter. --- foji/openapi/handler.go.tpl | 12 +++-- tests/example/http_handler_gen.go | 49 +++++++++++++++++--- tests/example/model_gen.go | 74 +++++++++++++++++++++++++++++++ tests/example/openapi.yaml | 21 +++++++++ tests/example/service.go | 5 +++ 5 files changed, 152 insertions(+), 9 deletions(-) diff --git a/foji/openapi/handler.go.tpl b/foji/openapi/handler.go.tpl index 2c461e4..db59481 100644 --- a/foji/openapi/handler.go.tpl +++ b/foji/openapi/handler.go.tpl @@ -12,8 +12,10 @@ {{ goToken (camel $param.Value.Name) -}} {{- if $.ParamIsOptionalType $param }} *{{ end }} {{ $.GetType $package $typeName $param.Value.Schema }}, {{- end -}} - {{- if and (not (empty $body)) (not ($.OpHasExtension $op "x-raw-request" )) }} - {{- $type := $.GetType $package (print $op.OperationID " Request") $body.Schema }} body {{ $type -}} + {{- if $.OpHasExtension $op "x-raw-body" }} + body io.ReadCloser + {{- else if not (empty $body) }} + body {{ $.GetType $package (print $op.OperationID " Request") $body.Schema }} {{- end -}} ) ( {{- $response := $.GetOpHappyResponseType $package .RuntimeParams.op}} @@ -356,8 +358,10 @@ func (h OpenAPIHandlers) {{ pascal $op.OperationID}}(w http.ResponseWriter, r *h } {{- end}} - {{- $hasBody := and (not (empty $opBody)) (not ($.OpHasExtension $op "x-raw-request" )) }} - {{- if $hasBody }} + {{- $hasBody := or (not (empty $opBody)) ($.OpHasExtension $op "x-raw-body" ) }} + {{- if $.OpHasExtension $op "x-raw-body" }} + body := r.Body + {{- else if not (empty $opBody) }} {{- $bodyType := $.GetType $package (print $op.OperationID " Request") $opBody.Schema}} {{- if $opBody.IsJson }} diff --git a/tests/example/http_handler_gen.go b/tests/example/http_handler_gen.go index 03a9ea1..c992a08 100644 --- a/tests/example/http_handler_gen.go +++ b/tests/example/http_handler_gen.go @@ -4,6 +4,7 @@ package example import ( "context" + "io" "net/http" "time" @@ -32,15 +33,22 @@ type Operations interface { GetAuthSimple2Maybe(ctx context.Context, user *ExampleAuth) error GetAuthComplexMaybe(ctx context.Context, user *ExampleAuth) error GetComplexSecurity(ctx context.Context, user *ExampleAuth) ([]TestInt, error) - AddForm(ctx context.Context, body AddFormRequest) (*FooBar, error) - AddMultipartForm(ctx context.Context, body AddMultipartFormRequest) (*FooBar, error) + AddForm(ctx context.Context, + body AddFormRequest) (*FooBar, error) + AddMultipartForm(ctx context.Context, + body AddMultipartFormRequest) (*FooBar, error) HeaderResponse(ctx context.Context) (http.Header, error) - AddInlinedAllOf(ctx context.Context, body AddInlinedAllOfRequest) (*FooBar, error) - AddInlinedBody(ctx context.Context, body AddInlinedBodyRequest) (*FooBar, error) + AddInlinedAllOf(ctx context.Context, + body AddInlinedAllOfRequest) (*FooBar, error) + AddInlinedBody(ctx context.Context, + body AddInlinedBodyRequest) (*FooBar, error) GetExampleParams(ctx context.Context, k1 string, k2 uuid.UUID, k3 time.Time, k4 int32, k5 int64, enumTest GetExampleParamsEnumTest) (*Example, error) - NoResponse(ctx context.Context, body Foo) error + NoResponse(ctx context.Context, + body Foo) error GetExampleOptional(ctx context.Context, k1 *string, k2 *uuid.UUID, k3 *time.Time, k4 *int32, k5 *int64, k5Default int64) (*Example, error) GetExampleQuery(ctx context.Context, k1 string, k2 uuid.UUID, k3 time.Time, k4 int32, k5 int64, k6 []string, k7 []uuid.UUID) (*Example, error) + GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, + body io.ReadCloser) (*Example, error) GetRawRequest(r *http.Request, vehicle GetRawRequestVehicle) (*Example, error) GetRawRequestResponse(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseVehicle) (*Example, error) GetRawRequestResponseAndHeaders(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseAndHeadersVehicle) (*Example, http.Header, error) @@ -124,6 +132,7 @@ func RegisterHTTP(ops Operations, r Mux, bearerAuth TokenAuthenticator, customHe r.Handle("POST /examples/noResponse", http.HandlerFunc(s.NoResponse)) r.Handle("GET /examples/optional", http.HandlerFunc(s.GetExampleOptional)) r.Handle("GET /examples/query", http.HandlerFunc(s.GetExampleQuery)) + r.Handle("GET /examples/rawBody", http.HandlerFunc(s.GetRawBody)) r.Handle("GET /examples/rawRequest", http.HandlerFunc(s.GetRawRequest)) r.Handle("GET /examples/rawRequestResponse", http.HandlerFunc(s.GetRawRequestResponse)) r.Handle("GET /examples/rawRequestResponseAndHeaders", http.HandlerFunc(s.GetRawRequestResponseAndHeaders)) @@ -665,6 +674,36 @@ func (h OpenAPIHandlers) GetExampleQuery(w http.ResponseWriter, r *http.Request) httputil.JSONWrite(w, r, 200, response) } +// GetRawBody +func (h OpenAPIHandlers) GetRawBody(w http.ResponseWriter, r *http.Request) { + var err error + + logctx.AddStrToContext(r.Context(), "op", "getRawBody") + + var validationErrors validation.Errors + + vehicle, _, err := params.GetEnumQuery(r, "vehicle", true, NewGetRawBodyVehicle) + if err != nil { + validationErrors.Add("vehicle", err) + } + + if validationErrors != nil { + httputil.ErrorHandler(w, r, validationErrors.GetErr()) + + return + } + body := r.Body + + response, err := h.ops.GetRawBody(r.Context(), vehicle, body) + if err != nil { + httputil.ErrorHandler(w, r, err) + + return + } + + httputil.JSONWrite(w, r, 200, response) +} + // GetRawRequest func (h OpenAPIHandlers) GetRawRequest(w http.ResponseWriter, r *http.Request) { var err error diff --git a/tests/example/model_gen.go b/tests/example/model_gen.go index a349ed8..ae1e18d 100644 --- a/tests/example/model_gen.go +++ b/tests/example/model_gen.go @@ -3039,6 +3039,80 @@ func (e *GetExampleParamsEnumTest) Scan(src interface{}) error { return nil } +// GetRawBodyVehicle +// Op: getRawBody Param: vehicle +type GetRawBodyVehicle int8 + +const ( + UnknownGetRawBodyVehicle GetRawBodyVehicle = iota + GetRawBodyVehicleCar + GetRawBodyVehicleTruck + GetRawBodyVehicleBike +) + +func NewGetRawBodyVehicle(name string) GetRawBodyVehicle { + switch name { + case "car": + return GetRawBodyVehicleCar + case "truck": + return GetRawBodyVehicleTruck + case "bike": + return GetRawBodyVehicleBike + } + + return GetRawBodyVehicle(0) +} + +var GetRawBodyVehicleString = map[GetRawBodyVehicle]string{ + GetRawBodyVehicleCar: "car", + GetRawBodyVehicleTruck: "truck", + GetRawBodyVehicleBike: "bike", +} + +func (e GetRawBodyVehicle) String() string { + return GetRawBodyVehicleString[e] +} + +func (e *GetRawBodyVehicle) UnmarshalJSON(input []byte) (err error) { + var i int8 + + err = json.Unmarshal(input, &i) + if err == nil { + *e = GetRawBodyVehicle(i) + return nil + } + + var s string + + err = json.Unmarshal(input, &s) + if err != nil { + return err + } + + *e = NewGetRawBodyVehicle(s) + + return nil +} + +func (e GetRawBodyVehicle) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +func (e GetRawBodyVehicle) Value() (driver.Value, error) { + return json.Marshal(e.String()) +} + +func (e *GetRawBodyVehicle) Scan(src interface{}) error { + s, ok := src.(string) + if !ok { + return fmt.Errorf("GetRawBodyVehicle.scan: scanned a %T, not []byte", src) //nolint + } + + *e = NewGetRawBodyVehicle(s) + + return nil +} + // GetRawRequestVehicle // Op: getRawRequest Param: vehicle type GetRawRequestVehicle int8 diff --git a/tests/example/openapi.yaml b/tests/example/openapi.yaml index c88f0cc..bda29dd 100644 --- a/tests/example/openapi.yaml +++ b/tests/example/openapi.yaml @@ -218,6 +218,27 @@ paths: application/json: schema: $ref: "#/components/schemas/Example" + /examples/rawBody: + get: + operationId: getRawBody + x-raw-body: true + parameters: + - name: vehicle + in: query + required: true + schema: + type: string + enum: + - car + - truck + - bike + responses: + "200": + description: 'Example' + content: + application/json: + schema: + $ref: "#/components/schemas/Example" /examples/rawResponse: get: operationId: getRawResponse diff --git a/tests/example/service.go b/tests/example/service.go index 4928aed..26e7dd8 100644 --- a/tests/example/service.go +++ b/tests/example/service.go @@ -2,6 +2,7 @@ package example import ( "context" + "io" "net/http" "time" @@ -74,6 +75,10 @@ func (s *Service) GetRawRequest(r *http.Request, vehicle GetRawRequestVehicle) ( return nil, nil } +func (s *Service) GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, body io.ReadCloser) (*Example, error) { + return nil, nil +} + func (s *Service) GetRawResponse(ctx context.Context, w http.ResponseWriter, vehicle GetRawResponseVehicle) (*Example, error) { return nil, nil } From 8b7195c737b55c1ed37d22767cceea036c450c32 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 8 Aug 2025 13:44:19 -0700 Subject: [PATCH 3/5] Remove extra linebreak --- foji/openapi/handler.go.tpl | 4 ++-- tests/example/http_handler_gen.go | 18 ++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/foji/openapi/handler.go.tpl b/foji/openapi/handler.go.tpl index db59481..0ceb58d 100644 --- a/foji/openapi/handler.go.tpl +++ b/foji/openapi/handler.go.tpl @@ -12,9 +12,9 @@ {{ goToken (camel $param.Value.Name) -}} {{- if $.ParamIsOptionalType $param }} *{{ end }} {{ $.GetType $package $typeName $param.Value.Schema }}, {{- end -}} - {{- if $.OpHasExtension $op "x-raw-body" }} + {{- if $.OpHasExtension $op "x-raw-body" -}} body io.ReadCloser - {{- else if not (empty $body) }} + {{- else if not (empty $body) -}} body {{ $.GetType $package (print $op.OperationID " Request") $body.Schema }} {{- end -}} ) ( diff --git a/tests/example/http_handler_gen.go b/tests/example/http_handler_gen.go index c992a08..4daf732 100644 --- a/tests/example/http_handler_gen.go +++ b/tests/example/http_handler_gen.go @@ -33,22 +33,16 @@ type Operations interface { GetAuthSimple2Maybe(ctx context.Context, user *ExampleAuth) error GetAuthComplexMaybe(ctx context.Context, user *ExampleAuth) error GetComplexSecurity(ctx context.Context, user *ExampleAuth) ([]TestInt, error) - AddForm(ctx context.Context, - body AddFormRequest) (*FooBar, error) - AddMultipartForm(ctx context.Context, - body AddMultipartFormRequest) (*FooBar, error) + AddForm(ctx context.Context, body AddFormRequest) (*FooBar, error) + AddMultipartForm(ctx context.Context, body AddMultipartFormRequest) (*FooBar, error) HeaderResponse(ctx context.Context) (http.Header, error) - AddInlinedAllOf(ctx context.Context, - body AddInlinedAllOfRequest) (*FooBar, error) - AddInlinedBody(ctx context.Context, - body AddInlinedBodyRequest) (*FooBar, error) + AddInlinedAllOf(ctx context.Context, body AddInlinedAllOfRequest) (*FooBar, error) + AddInlinedBody(ctx context.Context, body AddInlinedBodyRequest) (*FooBar, error) GetExampleParams(ctx context.Context, k1 string, k2 uuid.UUID, k3 time.Time, k4 int32, k5 int64, enumTest GetExampleParamsEnumTest) (*Example, error) - NoResponse(ctx context.Context, - body Foo) error + NoResponse(ctx context.Context, body Foo) error GetExampleOptional(ctx context.Context, k1 *string, k2 *uuid.UUID, k3 *time.Time, k4 *int32, k5 *int64, k5Default int64) (*Example, error) GetExampleQuery(ctx context.Context, k1 string, k2 uuid.UUID, k3 time.Time, k4 int32, k5 int64, k6 []string, k7 []uuid.UUID) (*Example, error) - GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, - body io.ReadCloser) (*Example, error) + GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, body io.ReadCloser) (*Example, error) GetRawRequest(r *http.Request, vehicle GetRawRequestVehicle) (*Example, error) GetRawRequestResponse(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseVehicle) (*Example, error) GetRawRequestResponseAndHeaders(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseAndHeadersVehicle) (*Example, http.Header, error) From 739c17b084af5b86fac0e1539914caf739e70d0c Mon Sep 17 00:00:00 2001 From: David Newgas Date: Wed, 24 Sep 2025 15:25:29 -0700 Subject: [PATCH 4/5] Test: getRawBody test should have a body, not params --- tests/example/http_handler_gen.go | 17 +------ tests/example/model_gen.go | 74 ------------------------------- tests/example/openapi.yaml | 15 +++---- tests/example/service.go | 2 +- 4 files changed, 8 insertions(+), 100 deletions(-) diff --git a/tests/example/http_handler_gen.go b/tests/example/http_handler_gen.go index 4daf732..c7bb310 100644 --- a/tests/example/http_handler_gen.go +++ b/tests/example/http_handler_gen.go @@ -42,7 +42,7 @@ type Operations interface { NoResponse(ctx context.Context, body Foo) error GetExampleOptional(ctx context.Context, k1 *string, k2 *uuid.UUID, k3 *time.Time, k4 *int32, k5 *int64, k5Default int64) (*Example, error) GetExampleQuery(ctx context.Context, k1 string, k2 uuid.UUID, k3 time.Time, k4 int32, k5 int64, k6 []string, k7 []uuid.UUID) (*Example, error) - GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, body io.ReadCloser) (*Example, error) + GetRawBody(ctx context.Context, body io.ReadCloser) (*Example, error) GetRawRequest(r *http.Request, vehicle GetRawRequestVehicle) (*Example, error) GetRawRequestResponse(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseVehicle) (*Example, error) GetRawRequestResponseAndHeaders(r *http.Request, w http.ResponseWriter, vehicle GetRawRequestResponseAndHeadersVehicle) (*Example, http.Header, error) @@ -673,22 +673,9 @@ func (h OpenAPIHandlers) GetRawBody(w http.ResponseWriter, r *http.Request) { var err error logctx.AddStrToContext(r.Context(), "op", "getRawBody") - - var validationErrors validation.Errors - - vehicle, _, err := params.GetEnumQuery(r, "vehicle", true, NewGetRawBodyVehicle) - if err != nil { - validationErrors.Add("vehicle", err) - } - - if validationErrors != nil { - httputil.ErrorHandler(w, r, validationErrors.GetErr()) - - return - } body := r.Body - response, err := h.ops.GetRawBody(r.Context(), vehicle, body) + response, err := h.ops.GetRawBody(r.Context(), body) if err != nil { httputil.ErrorHandler(w, r, err) diff --git a/tests/example/model_gen.go b/tests/example/model_gen.go index ae1e18d..a349ed8 100644 --- a/tests/example/model_gen.go +++ b/tests/example/model_gen.go @@ -3039,80 +3039,6 @@ func (e *GetExampleParamsEnumTest) Scan(src interface{}) error { return nil } -// GetRawBodyVehicle -// Op: getRawBody Param: vehicle -type GetRawBodyVehicle int8 - -const ( - UnknownGetRawBodyVehicle GetRawBodyVehicle = iota - GetRawBodyVehicleCar - GetRawBodyVehicleTruck - GetRawBodyVehicleBike -) - -func NewGetRawBodyVehicle(name string) GetRawBodyVehicle { - switch name { - case "car": - return GetRawBodyVehicleCar - case "truck": - return GetRawBodyVehicleTruck - case "bike": - return GetRawBodyVehicleBike - } - - return GetRawBodyVehicle(0) -} - -var GetRawBodyVehicleString = map[GetRawBodyVehicle]string{ - GetRawBodyVehicleCar: "car", - GetRawBodyVehicleTruck: "truck", - GetRawBodyVehicleBike: "bike", -} - -func (e GetRawBodyVehicle) String() string { - return GetRawBodyVehicleString[e] -} - -func (e *GetRawBodyVehicle) UnmarshalJSON(input []byte) (err error) { - var i int8 - - err = json.Unmarshal(input, &i) - if err == nil { - *e = GetRawBodyVehicle(i) - return nil - } - - var s string - - err = json.Unmarshal(input, &s) - if err != nil { - return err - } - - *e = NewGetRawBodyVehicle(s) - - return nil -} - -func (e GetRawBodyVehicle) MarshalJSON() ([]byte, error) { - return json.Marshal(e.String()) -} - -func (e GetRawBodyVehicle) Value() (driver.Value, error) { - return json.Marshal(e.String()) -} - -func (e *GetRawBodyVehicle) Scan(src interface{}) error { - s, ok := src.(string) - if !ok { - return fmt.Errorf("GetRawBodyVehicle.scan: scanned a %T, not []byte", src) //nolint - } - - *e = NewGetRawBodyVehicle(s) - - return nil -} - // GetRawRequestVehicle // Op: getRawRequest Param: vehicle type GetRawRequestVehicle int8 diff --git a/tests/example/openapi.yaml b/tests/example/openapi.yaml index bda29dd..d52a3ea 100644 --- a/tests/example/openapi.yaml +++ b/tests/example/openapi.yaml @@ -222,16 +222,11 @@ paths: get: operationId: getRawBody x-raw-body: true - parameters: - - name: vehicle - in: query - required: true - schema: - type: string - enum: - - car - - truck - - bike + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Foo' responses: "200": description: 'Example' diff --git a/tests/example/service.go b/tests/example/service.go index 26e7dd8..ed0dea8 100644 --- a/tests/example/service.go +++ b/tests/example/service.go @@ -75,7 +75,7 @@ func (s *Service) GetRawRequest(r *http.Request, vehicle GetRawRequestVehicle) ( return nil, nil } -func (s *Service) GetRawBody(ctx context.Context, vehicle GetRawBodyVehicle, body io.ReadCloser) (*Example, error) { +func (s *Service) GetRawBody(ctx context.Context, body io.ReadCloser) (*Example, error) { return nil, nil } From bf3fef98fbc64afbc438925bc0d778ead1481f3a Mon Sep 17 00:00:00 2001 From: David Newgas Date: Wed, 24 Sep 2025 20:33:56 -0700 Subject: [PATCH 5/5] Lint --- cfg/merge.go | 2 +- input/proto/parse.go | 1 + runtime/funcs.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cfg/merge.go b/cfg/merge.go index 2715025..076b1d6 100644 --- a/cfg/merge.go +++ b/cfg/merge.go @@ -161,7 +161,7 @@ func (f FileInput) Merge(from FileInput) FileInput { return from } -// Merge merges all properties from an ancestor TypeMap. +// MergeTypesMaps merges all properties from an ancestor TypeMap. func MergeTypesMaps(maps ...stringlist.StringMap) stringlist.StringMap { result := stringlist.StringMap{} diff --git a/input/proto/parse.go b/input/proto/parse.go index 0a81fb2..5c964b5 100644 --- a/input/proto/parse.go +++ b/input/proto/parse.go @@ -151,6 +151,7 @@ func (d *PBFile) VisitMessage(n *parser.Message) bool { } // The rest of these are required by the visitor interface. + func (d *PBFile) VisitExtend(_ *parser.Extend) bool { return true } diff --git a/runtime/funcs.go b/runtime/funcs.go index 816b576..b4d59f2 100644 --- a/runtime/funcs.go +++ b/runtime/funcs.go @@ -171,7 +171,7 @@ func ToSlice(vv ...any) any { return ss } -// numbers returns a slice of strings of the numbers start to end (inclusive). +// Numbers returns a slice of strings of the numbers start to end (inclusive). func Numbers(start, end int) stringlist.Strings { var ss stringlist.Strings