mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-07 02:32:13 +00:00
🔥 v3: update Ctx.Format to match Express's res.format (#2766)
* 🔥 v3: update Ctx.Format to match Express's res.format
While the existing Ctx.Format provides a concise convenience method for
basic content negotiation on simple structures, res.format allows
developers to set their own custom handlers for each content type.
The existing Ctx.Format is renamed to Ctx.AutoFormat.
* doc: add docs for Ctx.Format
* refactor: update based on code review feedback
- Rename Fmt to ResFmt
- Add comments in several places
- Return errors instead of panicking in Format
- Add 'Accept' to the Vary header in Format to match res.format
* chore: improve docs and tests for AutoFormat and Format
This commit is contained in:
parent
2954e3bbae
commit
408fa20a91
60
ctx.go
60
ctx.go
@ -105,6 +105,12 @@ type Views interface {
|
||||
Render(io.Writer, string, any, ...string) error
|
||||
}
|
||||
|
||||
// ResFmt associates a Content Type to a fiber.Handler for c.Format
|
||||
type ResFmt struct {
|
||||
MediaType string
|
||||
Handler func(Ctx) error
|
||||
}
|
||||
|
||||
// Accepts checks if the specified extensions or content types are acceptable.
|
||||
func (c *DefaultCtx) Accepts(offers ...string) string {
|
||||
return getOffer(c.Get(HeaderAccept), acceptsOfferType, offers...)
|
||||
@ -375,9 +381,61 @@ func (c *DefaultCtx) Response() *fasthttp.Response {
|
||||
}
|
||||
|
||||
// Format performs content-negotiation on the Accept HTTP header.
|
||||
// It uses Accepts to select a proper format and calls the matching
|
||||
// user-provided handler function.
|
||||
// If no accepted format is found, and a format with MediaType "default" is given,
|
||||
// that default handler is called. If no format is found and no default is given,
|
||||
// StatusNotAcceptable is sent.
|
||||
func (c *DefaultCtx) Format(handlers ...ResFmt) error {
|
||||
if len(handlers) == 0 {
|
||||
return ErrNoHandlers
|
||||
}
|
||||
|
||||
c.Vary(HeaderAccept)
|
||||
|
||||
if c.Get(HeaderAccept) == "" {
|
||||
c.Response().Header.SetContentType(handlers[0].MediaType)
|
||||
return handlers[0].Handler(c)
|
||||
}
|
||||
|
||||
// Using an int literal as the slice capacity allows for the slice to be
|
||||
// allocated on the stack. The number was chosen arbitrarily as an
|
||||
// approximation of the maximum number of content types a user might handle.
|
||||
// If the user goes over, it just causes allocations, so it's not a problem.
|
||||
types := make([]string, 0, 8)
|
||||
var defaultHandler Handler
|
||||
for _, h := range handlers {
|
||||
if h.MediaType == "default" {
|
||||
defaultHandler = h.Handler
|
||||
continue
|
||||
}
|
||||
types = append(types, h.MediaType)
|
||||
}
|
||||
accept := c.Accepts(types...)
|
||||
|
||||
if accept == "" {
|
||||
if defaultHandler == nil {
|
||||
return c.SendStatus(StatusNotAcceptable)
|
||||
}
|
||||
return defaultHandler(c)
|
||||
}
|
||||
|
||||
for _, h := range handlers {
|
||||
if h.MediaType == accept {
|
||||
c.Response().Header.SetContentType(h.MediaType)
|
||||
return h.Handler(c)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: format: an Accept was found but no handler was called", errUnreachable)
|
||||
}
|
||||
|
||||
// AutoFormat performs content-negotiation on the Accept HTTP header.
|
||||
// It uses Accepts to select a proper format.
|
||||
// The supported content types are text/html, text/plain, application/json, and application/xml.
|
||||
// For more flexible content negotiation, use Format.
|
||||
// If the header is not specified or there is no proper format, text/plain is used.
|
||||
func (c *DefaultCtx) Format(body any) error {
|
||||
func (c *DefaultCtx) AutoFormat(body any) error {
|
||||
// Get accepted content type
|
||||
accept := c.Accepts("html", "json", "txt", "xml")
|
||||
// Set accepted content type
|
||||
|
@ -90,9 +90,19 @@ type Ctx interface {
|
||||
Response() *fasthttp.Response
|
||||
|
||||
// Format performs content-negotiation on the Accept HTTP header.
|
||||
// It uses Accepts to select a proper format and calls the matching
|
||||
// user-provided handler function.
|
||||
// If no accepted format is found, and a format with MediaType "default" is given,
|
||||
// that default handler is called. If no format is found and no default is given,
|
||||
// StatusNotAcceptable is sent.
|
||||
Format(handlers ...ResFmt) error
|
||||
|
||||
// AutoFormat performs content-negotiation on the Accept HTTP header.
|
||||
// It uses Accepts to select a proper format.
|
||||
// The supported content types are text/html, text/plain, application/json, and application/xml.
|
||||
// For more flexible content negotiation, use Format.
|
||||
// If the header is not specified or there is no proper format, text/plain is used.
|
||||
Format(body any) error
|
||||
AutoFormat(body any) error
|
||||
|
||||
// FormFile returns the first file by key from a MultipartForm.
|
||||
FormFile(key string) (*multipart.FileHeader, error)
|
||||
|
189
ctx_test.go
189
ctx_test.go
@ -717,49 +717,198 @@ func Test_Ctx_Format(t *testing.T) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
// set `accepted` to whatever media type was chosen by Format
|
||||
var accepted string
|
||||
formatHandlers := func(types ...string) []ResFmt {
|
||||
fmts := []ResFmt{}
|
||||
for _, t := range types {
|
||||
t := utils.CopyString(t)
|
||||
fmts = append(fmts, ResFmt{t, func(c Ctx) error {
|
||||
accepted = t
|
||||
return nil
|
||||
}})
|
||||
}
|
||||
return fmts
|
||||
}
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`)
|
||||
err := c.Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...)
|
||||
require.Equal(t, "application/xhtml+xml", accepted)
|
||||
require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode())
|
||||
|
||||
err = c.Format(formatHandlers("foo/bar;a=b")...)
|
||||
require.Equal(t, "foo/bar;a=b", accepted)
|
||||
require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode())
|
||||
|
||||
myError := errors.New("this is an error")
|
||||
err = c.Format(ResFmt{"text/html", func(c Ctx) error { return myError }})
|
||||
require.ErrorIs(t, err, myError)
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, "application/json")
|
||||
err = c.Format(ResFmt{"text/html", func(c Ctx) error { return c.SendStatus(StatusOK) }})
|
||||
require.Equal(t, StatusNotAcceptable, c.Response().StatusCode())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Format(formatHandlers("text/html", "default")...)
|
||||
require.Equal(t, "default", accepted)
|
||||
require.Equal(t, "text/html", c.GetRespHeader(HeaderContentType))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Format()
|
||||
require.ErrorIs(t, err, ErrNoHandlers)
|
||||
}
|
||||
|
||||
func Benchmark_Ctx_Format(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
c.Request().Header.Set(HeaderAccept, "application/json,text/plain; format=flowed; q=0.9")
|
||||
|
||||
fail := func(_ Ctx) error {
|
||||
require.FailNow(b, "Wrong type chosen")
|
||||
return errors.New("Wrong type chosen")
|
||||
}
|
||||
ok := func(_ Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
b.Run("with arg allocation", func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format(
|
||||
ResFmt{"application/xml", fail},
|
||||
ResFmt{"text/html", fail},
|
||||
ResFmt{"text/plain;format=fixed", fail},
|
||||
ResFmt{"text/plain;format=flowed", ok},
|
||||
)
|
||||
}
|
||||
require.NoError(b, err)
|
||||
})
|
||||
|
||||
b.Run("pre-allocated args", func(b *testing.B) {
|
||||
offers := []ResFmt{
|
||||
{"application/xml", fail},
|
||||
{"text/html", fail},
|
||||
{"text/plain;format=fixed", fail},
|
||||
{"text/plain;format=flowed", ok},
|
||||
}
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format(offers...)
|
||||
}
|
||||
require.NoError(b, err)
|
||||
})
|
||||
|
||||
c.Request().Header.Set("Accept", "text/plain")
|
||||
b.Run("text/plain", func(b *testing.B) {
|
||||
offers := []ResFmt{
|
||||
{"application/xml", fail},
|
||||
{"text/plain", ok},
|
||||
}
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format(offers...)
|
||||
}
|
||||
require.NoError(b, err)
|
||||
})
|
||||
|
||||
c.Request().Header.Set("Accept", "json")
|
||||
b.Run("json", func(b *testing.B) {
|
||||
offers := []ResFmt{
|
||||
{"xml", fail},
|
||||
{"html", fail},
|
||||
{"json", ok},
|
||||
}
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format(offers...)
|
||||
}
|
||||
require.NoError(b, err)
|
||||
})
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_AutoFormat
|
||||
func Test_Ctx_AutoFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMETextPlain)
|
||||
err := c.Format([]byte("Hello, World!"))
|
||||
err := c.AutoFormat([]byte("Hello, World!"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Hello, World!", string(c.Response().Body()))
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "<p>Hello, World!</p>", string(c.Response().Body()))
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON)
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `"Hello, World!"`, string(c.Response().Body()))
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMETextPlain)
|
||||
err = c.Format(complex(1, 1))
|
||||
err = c.AutoFormat(complex(1, 1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "(1+1i)", string(c.Response().Body()))
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMEApplicationXML)
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `<string>Hello, World!</string>`, string(c.Response().Body()))
|
||||
|
||||
err = c.Format(complex(1, 1))
|
||||
err = c.AutoFormat(complex(1, 1))
|
||||
require.Error(t, err)
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMETextPlain)
|
||||
err = c.Format(Map{})
|
||||
err = c.AutoFormat(Map{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "map[]", string(c.Response().Body()))
|
||||
|
||||
type broken string
|
||||
c.Request().Header.Set(HeaderAccept, "broken/accept")
|
||||
require.NoError(t, err)
|
||||
err = c.Format(broken("Hello, World!"))
|
||||
err = c.AutoFormat(broken("Hello, World!"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `Hello, World!`, string(c.Response().Body()))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_Format -benchmem -count=4
|
||||
func Benchmark_Ctx_Format(b *testing.B) {
|
||||
func Test_Ctx_AutoFormat_Struct(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Message struct {
|
||||
Recipients []string
|
||||
Sender string `xml:"sender,attr"`
|
||||
Urgency int `xml:"urgency,attr"`
|
||||
}
|
||||
data := Message{
|
||||
Recipients: []string{"Alice", "Bob"},
|
||||
Sender: "Carol",
|
||||
Urgency: 3,
|
||||
}
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON)
|
||||
err := c.AutoFormat(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
`{"Recipients":["Alice","Bob"],"Sender":"Carol","Urgency":3}`,
|
||||
string(c.Response().Body()),
|
||||
)
|
||||
|
||||
c.Request().Header.Set(HeaderAccept, MIMEApplicationXML)
|
||||
err = c.AutoFormat(data)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
`<Message sender="Carol" urgency="3"><Recipients>Alice</Recipients><Recipients>Bob</Recipients></Message>`,
|
||||
string(c.Response().Body()),
|
||||
)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat -benchmem -count=4
|
||||
func Benchmark_Ctx_AutoFormat(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
@ -769,14 +918,14 @@ func Benchmark_Ctx_Format(b *testing.B) {
|
||||
|
||||
var err error
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
}
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, `Hello, World!`, string(c.Response().Body()))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_Format_HTML -benchmem -count=4
|
||||
func Benchmark_Ctx_Format_HTML(b *testing.B) {
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_HTML -benchmem -count=4
|
||||
func Benchmark_Ctx_AutoFormat_HTML(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
@ -786,14 +935,14 @@ func Benchmark_Ctx_Format_HTML(b *testing.B) {
|
||||
|
||||
var err error
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
}
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "<p>Hello, World!</p>", string(c.Response().Body()))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_Format_JSON -benchmem -count=4
|
||||
func Benchmark_Ctx_Format_JSON(b *testing.B) {
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_JSON -benchmem -count=4
|
||||
func Benchmark_Ctx_AutoFormat_JSON(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
@ -803,14 +952,14 @@ func Benchmark_Ctx_Format_JSON(b *testing.B) {
|
||||
|
||||
var err error
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
}
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, `"Hello, World!"`, string(c.Response().Body()))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_Format_XML -benchmem -count=4
|
||||
func Benchmark_Ctx_Format_XML(b *testing.B) {
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_XML -benchmem -count=4
|
||||
func Benchmark_Ctx_AutoFormat_XML(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
@ -820,7 +969,7 @@ func Benchmark_Ctx_Format_XML(b *testing.B) {
|
||||
|
||||
var err error
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = c.Format("Hello, World!")
|
||||
err = c.AutoFormat("Hello, World!")
|
||||
}
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, `<string>Hello, World!</string>`, string(c.Response().Body()))
|
||||
|
@ -184,6 +184,47 @@ app.Get("/", func(c *fiber.Ctx) error {
|
||||
})
|
||||
```
|
||||
|
||||
## AutoFormat
|
||||
|
||||
Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format.
|
||||
The supported content types are `text/html`, `text/plain`, `application/json`, and `application/xml`.
|
||||
For more flexible content negotiation, use [Format](ctx.md#format).
|
||||
|
||||
|
||||
:::info
|
||||
If the header is **not** specified or there is **no** proper format, **text/plain** is used.
|
||||
:::
|
||||
|
||||
```go title="Signature"
|
||||
func (c *Ctx) AutoFormat(body any) error
|
||||
```
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
// Accept: text/plain
|
||||
c.AutoFormat("Hello, World!")
|
||||
// => Hello, World!
|
||||
|
||||
// Accept: text/html
|
||||
c.AutoFormat("Hello, World!")
|
||||
// => <p>Hello, World!</p>
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
user := User{"John Doe"}
|
||||
|
||||
// Accept: application/json
|
||||
c.AutoFormat(user)
|
||||
// => {"Name":"John Doe"}
|
||||
|
||||
// Accept: application/xml
|
||||
c.AutoFormat(user)
|
||||
// => <User><Name>John Doe</Name></User>
|
||||
// ..
|
||||
})
|
||||
```
|
||||
|
||||
## BaseURL
|
||||
|
||||
Returns the base URL \(**protocol** + **host**\) as a `string`.
|
||||
@ -510,30 +551,54 @@ app.Get("/", func(c *fiber.Ctx) error {
|
||||
|
||||
## Format
|
||||
|
||||
Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format.
|
||||
Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format from the supplied offers. A default handler can be provided by setting the `MediaType` to `"default"`. If no offers match and no default is provided, a 406 (Not Acceptable) response is sent. The Content-Type is automatically set when a handler is selected.
|
||||
|
||||
:::info
|
||||
If the header is **not** specified or there is **no** proper format, **text/plain** is used.
|
||||
If the Accept header is **not** specified, the first handler will be used.
|
||||
:::
|
||||
|
||||
```go title="Signature"
|
||||
func (c *Ctx) Format(body interface{}) error
|
||||
func (c *Ctx) Format(handlers ...ResFmt) error
|
||||
```
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
// Accept: text/plain
|
||||
c.Format("Hello, World!")
|
||||
// => Hello, World!
|
||||
// Accept: application/json => {"command":"eat","subject":"fruit"}
|
||||
// Accept: text/plain => Eat Fruit!
|
||||
// Accept: application/xml => Not Acceptable
|
||||
app.Get("/no-default", func(c fiber.Ctx) error {
|
||||
return c.Format(
|
||||
fiber.ResFmt{"application/json", func(c fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"command": "eat",
|
||||
"subject": "fruit",
|
||||
})
|
||||
}},
|
||||
fiber.ResFmt{"text/plain", func(c fiber.Ctx) error {
|
||||
return c.SendString("Eat Fruit!")
|
||||
}},
|
||||
)
|
||||
})
|
||||
|
||||
// Accept: text/html
|
||||
c.Format("Hello, World!")
|
||||
// => <p>Hello, World!</p>
|
||||
// Accept: application/json => {"command":"eat","subject":"fruit"}
|
||||
// Accept: text/plain => Eat Fruit!
|
||||
// Accept: application/xml => Eat Fruit!
|
||||
app.Get("/default", func(c fiber.Ctx) error {
|
||||
textHandler := func(c fiber.Ctx) error {
|
||||
return c.SendString("Eat Fruit!")
|
||||
}
|
||||
|
||||
// Accept: application/json
|
||||
c.Format("Hello, World!")
|
||||
// => "Hello, World!"
|
||||
// ..
|
||||
handlers := []fiber.ResFmt{
|
||||
{"application/json", func(c fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"command": "eat",
|
||||
"subject": "fruit",
|
||||
})
|
||||
}},
|
||||
{"text/plain", textHandler},
|
||||
{"default", textHandler},
|
||||
}
|
||||
|
||||
return c.Format(handlers...)
|
||||
})
|
||||
```
|
||||
|
||||
|
10
error.go
10
error.go
@ -7,6 +7,10 @@ import (
|
||||
"github.com/gofiber/fiber/v3/internal/schema"
|
||||
)
|
||||
|
||||
// Wrap and return this for unreachable code if panicking is undesirable (i.e., in a handler).
|
||||
// Unexported because users will hopefully never need to see it.
|
||||
var errUnreachable = stdErrors.New("fiber: unreachable code, please create an issue at github.com/gofiber/fiber")
|
||||
|
||||
// Graceful shutdown errors
|
||||
var (
|
||||
ErrGracefulTimeout = stdErrors.New("shutdown: graceful timeout has been reached, exiting")
|
||||
@ -26,6 +30,12 @@ var (
|
||||
// Binder errors
|
||||
var ErrCustomBinderNotFound = stdErrors.New("binder: custom binder not found, please be sure to enter the right name")
|
||||
|
||||
// Format errors
|
||||
var (
|
||||
// ErrNoHandlers is returned when c.Format is called with no arguments.
|
||||
ErrNoHandlers = stdErrors.New("format: at least one handler is required, but none were set")
|
||||
)
|
||||
|
||||
// gorilla/schema errors
|
||||
type (
|
||||
// ConversionError Conversion error exposes the internal schema.ConversionError for public use.
|
||||
|
Loading…
x
Reference in New Issue
Block a user