feat: add RawBody support and UnsupportedMediaType response

This commit is contained in:
Daniel G. Taylor 2022-05-26 16:40:59 -07:00
parent 73c101e7f0
commit 0a73c646f1
No known key found for this signature in database
GPG key ID: 74AE195C5112E534
5 changed files with 59 additions and 1 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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,