From 0a73c646f17e6eb2ff62cb1a01b11334a976faea Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Thu, 26 May 2022 16:40:59 -0700 Subject: [PATCH] feat: add RawBody support and UnsupportedMediaType response --- README.md | 14 +++++++++++++- resolver.go | 6 ++++++ resolver_test.go | 33 +++++++++++++++++++++++++++++++++ responses/responses.go | 5 +++++ responses/responses_test.go | 2 ++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f1b2a04e..48a1b258 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ The following types are supported out of the box: For example, if the parameter is a query param and the type is `[]string` it might look like `?tags=tag1,tag2` in the URI. -The special struct field `Body` will be treated as the input request body and can refer to another struct or you can embed a struct inline. +The special struct field `Body` will be treated as the input request body and can refer to another struct or you can embed a struct inline. `RawBody` can also be used to provide access to the `[]byte` used to validate & load `Body`. Here is an example: @@ -472,6 +472,18 @@ op.NoBodyReadTimeout() op.Run(...) ``` +If you just need access to the input body bytes and still want to use the built-in JSON Schema validation, then you can instead use the `RawBody` input struct field. + +```go +type MyBody struct { + // This will generate JSON Schema, validate the input, and parse it. + Body MyStruct + + // This will contain the raw bytes used to load the above. + RawBody []byte +} +``` + ### Resolvers Sometimes the built-in validation isn't sufficient for your use-case, or you want to do something more complex with the incoming request object. This is where resolvers come in. diff --git a/resolver.go b/resolver.go index d16f40c0..9f81e65f 100644 --- a/resolver.go +++ b/resolver.go @@ -251,6 +251,12 @@ func setFields(ctx *hcontext, req *http.Request, input reflect.Value, t reflect. Value: string(data), }) } + + // If requested, also provide access to the raw body bytes. + if _, ok := t.FieldByName("RawBody"); ok { + input.FieldByName("RawBody").Set(reflect.ValueOf(data)) + } + continue } diff --git a/resolver_test.go b/resolver_test.go index 8f668e87..ce6088da 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -327,3 +327,36 @@ func TestStringQueryEmpty(t *testing.T) { assert.Equal(t, o.BooleanParam, true) assert.Equal(t, o.OtherParam, "") } + +func TestRawBody(t *testing.T) { + app := newTestRouter() + + app.Resource("/").Get("test", "Test", + NewResponse(http.StatusOK, "desc"), + ).Run(func(ctx Context, input struct { + Body struct { + Name string `json:"name"` + Tags []string `json:"tags"` + } + RawBody []byte + }) { + ctx.Write(input.RawBody) + }) + + // Note the weird formatting + body := `{ "name" : "Huma","tags": [ "one" ,"two"]}` + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", strings.NewReader(body)) + app.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Result().StatusCode) + assert.Equal(t, body, w.Body.String()) + + // Invalid input should still fail validation! + w = httptest.NewRecorder() + r, _ = http.NewRequest(http.MethodGet, "/", strings.NewReader("{}")) + app.ServeHTTP(w, r) + + assert.Equal(t, http.StatusUnprocessableEntity, w.Result().StatusCode) +} diff --git a/responses/responses.go b/responses/responses.go index b6c8f8f4..52b5dc1b 100644 --- a/responses/responses.go +++ b/responses/responses.go @@ -113,6 +113,11 @@ func RequestEntityTooLarge() huma.Response { return errorResponse(http.StatusRequestEntityTooLarge) } +// UnsupportedMediaType HTTP 415 response with a structured error body (e.g. JSON). +func UnsupportedMediaType() huma.Response { + return errorResponse(http.StatusUnsupportedMediaType) +} + // UnprocessableEntity HTTP 422 response with a structured error body (e.g. JSON). func UnprocessableEntity() huma.Response { return errorResponse(http.StatusUnprocessableEntity) diff --git a/responses/responses_test.go b/responses/responses_test.go index f037580f..cfef99d3 100644 --- a/responses/responses_test.go +++ b/responses/responses_test.go @@ -33,6 +33,7 @@ var funcs = struct { RequestTimeout, Conflict, PreconditionFailed, + UnsupportedMediaType, RequestEntityTooLarge, UnprocessableEntity, PreconditionRequired, @@ -72,6 +73,7 @@ func TestResponses(t *testing.T) { http.StatusConflict, http.StatusPreconditionFailed, http.StatusRequestEntityTooLarge, + http.StatusUnsupportedMediaType, http.StatusUnprocessableEntity, http.StatusPreconditionRequired, http.StatusInternalServerError,