Documentation
¶
Overview ¶
Package nvelope provides injection handlers that make building HTTP endpoints simple. In combination with npoint and nject it provides a API endpoint framework.
The main things it provides are a request decoder and a response encoder.
The request decoder will fill in a struct to capture all the parts of the request: path parameters, query parameters, headers, and the body. The decoding is driven by struct tags that are interpreted at program startup.
The response encoder is comparatively simpler: given a model and an error, it encodes the error or the model appropriately.
Deferred writer allows output to be buffered and then abandoned.
NotFound, Forbidden, and BadRequest provide easy ways to annotate an error return to cause a specific HTTP error code to be sent.
CatchPanic makes it easy to turn panics into error returns.
The provided example puts it all together.
Example ¶
Example shows an injection chain handling a single endpoint using nject, nape, and nvelope.
package main import ( "errors" "fmt" "io" "log" "net/http" "net/http/httptest" "strings" "github.com/muir/nape" "github.com/muir/nvelope" "github.com/gorilla/mux" ) // nolint:deadcode,unused func Main() { r := mux.NewRouter() srv := &http.Server{ Addr: "0.0.0.0:8080", Handler: r, } Service(r) log.Fatal(srv.ListenAndServe()) } type PostBodyModel struct { Use string `json:"use"` Exported string `json:"exported"` Names string `json:"names"` } type ExampleRequestBundle struct { Request PostBodyModel `nvelope:"model"` With *string `nvelope:"path,name=with"` Parameters int64 `nvelope:"path,name=parameters"` Friends []int `nvelope:"query,name=friends"` ContentType string `nvelope:"header,name=Content-Type"` } type ExampleResponse struct { Stuff string `json:"stuff,omitempty"` Here string `json:"here,omitempty"` } func HandleExampleEndpoint(req ExampleRequestBundle) (nvelope.Response, error) { if req.ContentType != "application/json" { return nil, errors.New("content type must be application/json") } switch req.Parameters { case 666: panic("something is not right") case 100: return nil, nil default: return ExampleResponse{ Stuff: *req.With, }, nil } } func Service(router *mux.Router) { service := nape.RegisterServiceWithMux("example", router) service.RegisterEndpoint("/a/path/{with}/{parameters}", // order matters and this is a correct order nvelope.NoLogger, nvelope.InjectWriter, nvelope.EncodeJSON, nvelope.CatchPanic, nvelope.Nil204, nvelope.ReadBody, nape.DecodeJSON, HandleExampleEndpoint, ).Methods("POST") } // Example shows an injection chain handling a single endpoint using nject, // nape, and nvelope. func main() { r := mux.NewRouter() Service(r) ts := httptest.NewServer(r) client := ts.Client() doPost := func(url string, body string) { // nolint:noctx res, err := client.Post(ts.URL+url, "application/json", strings.NewReader(body)) if err != nil { fmt.Println("response error:", err) return } b, err := io.ReadAll(res.Body) if err != nil { fmt.Println("read error:", err) return } res.Body.Close() fmt.Println(res.StatusCode, "->"+string(b)) } doPost("/a/path/joe/37", `{"Use":"yeah","Exported":"uh hu"}`) doPost("/a/path/joe/100", `{"Use":"yeah","Exported":"uh hu"}`) doPost("/a/path/joe/38", `invalid json`) doPost("/a/path/joe/666", `{"Use":"yeah","Exported":"uh hu"}`) }
Output: 200 ->{"stuff":"joe"} 204 -> 400 ->nvelope_test.ExampleRequestBundle model: Could not decode application/json into nvelope_test.PostBodyModel: invalid character 'i' looking for beginning of value 500 ->panic: something is not right
Index ¶
- Variables
- func BadRequest(err error) error
- func Forbidden(err error) error
- func GenerateDecoder(genOpts ...DecodeInputsGeneratorOpt) interface{}
- func GetReturnCode(err error) int
- func LoggerFromStd(log StdLogger) func() BasicLogger
- func MakeResponseEncoder(name string, encoderFuncArgs ...ResponseEncoderFuncArg) nject.Provider
- func MiddlewareBaseWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider
- func MiddlewareDeferredWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider
- func MiddlewareHandlerBaseWriter(m ...func(http.Handler) http.Handler) nject.Provider
- func MiddlewareHandlerDeferredWriter(m ...func(http.Handler) http.Handler) nject.Provider
- func NotFound(err error) error
- func RecoverInterface(err error) interface{}
- func RecoverStack(err error) string
- func ReturnCode(err error, code int) error
- func SetErrorOnPanic(ep *error, log BasicLogger)
- func Unauthorized(err error) error
- type APIEnforcerFunc
- type BasicLogger
- type Body
- type CanModel
- type DecodeInputsGeneratorOpt
- func RejectUnknownQueryParameters(b bool) DecodeInputsGeneratorOpt
- func WithDecoder(contentType string, decoder Decoder) DecodeInputsGeneratorOpt
- func WithDefaultContentType(contentType string) DecodeInputsGeneratorOpt
- func WithPathVarsFunction(pathVarFunction interface{}) DecodeInputsGeneratorOpt
- func WithTag(tag string) DecodeInputsGeneratorOpt
- type Decoder
- type DeferredWriter
- func (w *DeferredWriter) Body() ([]byte, int, error)
- func (w *DeferredWriter) Done() bool
- func (w *DeferredWriter) Flush() error
- func (w *DeferredWriter) FlushIfNotFlushed() error
- func (w *DeferredWriter) Header() http.Header
- func (w *DeferredWriter) PreserveHeader()
- func (w *DeferredWriter) Reset() error
- func (w *DeferredWriter) UnderlyingWriter() http.ResponseWriter
- func (w *DeferredWriter) Write(b []byte) (int, error)
- func (w *DeferredWriter) WriteHeader(statusCode int)
- type EncoderSpecificFuncArg
- type ErrorTranformer
- type LogFlusher
- type Response
- type ResponseEncoderFuncArg
- type RouteVarLookup
- type StdLogger
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var AutoFlushWriter = nject.Provide("autoflush-writer", func(inner func(), w *DeferredWriter) {
inner()
_ = w.FlushIfNotFlushed()
})
AutoFlushWriter calls Flush on the deferred writer if it hasn't already been done
var CatchPanic = nject.Provide("catch-panic", catchPanicInjector)
CatchPanic is a wrapper that catches downstream panics and returns an error a downsteam provider panic's.
var DebugIncludeExclude = nject.Required(nject.Provide("debug-include/exclude", func(log BasicLogger, d *nject.Debugging) { log.Debug(strings.Join(d.IncludeExclude, "\n")) }))
DebugIncludeExclude is a tiny wrapper around nject.Debugging. It logs the IncludeExclude strings.
var EncodeJSON = MakeResponseEncoder("JSON", WithEncoder("application/json", json.Marshal, WithEncoderErrorTransform(func(err error) (interface{}, bool) { var jm json.Marshaler if errors.As(err, &jm) { return jm, true } return nil, false }), ))
EncodeJSON is a JSON encoder manufactured by MakeResponseEncoder with default options.
var EncodeXML = MakeResponseEncoder("XML", WithEncoder("application/xml", xml.Marshal, WithEncoderErrorTransform(func(err error) (interface{}, bool) { var me xml.Marshaler if errors.As(err, &me) { return me, true } return nil, false }), ))
EncodeXML is a XML encoder manufactured by MakeResponseEncoder with default options.
var InjectWriter = nject.Provide("writer", NewDeferredWriter)
InjectWriter injects a DeferredWriter
var MinimalErrorHandler = nject.Provide("minimal-error-handler", minimalErrorHandler)
MinimalErrorHandler provides a way to catch returned error values from the many functions that return them if MakeResponseEncoder is not used. http.ResponseWriter is used instead of a DeferredWriter. That means that MinimalErrorHandler cannot know if a response has already been made. The assumption is that if the returned error is nil, a respons has been made and if the returned error is not nil, then a response has not yet been made and the MinimalErrorHandler should make one. GetReturnCode is used to determine the return code.
var Nil204 = nject.Desired(nject.Provide("nil-204", nil204))
Nil204 is a wrapper that causes looks for return values of Response and error and if both are nil, writes a 204 header and no data. It is mean to be used downstream from a response encocder.
var ReadBody = nject.Provide("read-body", readBody)
ReadBody is a provider that reads the input body from an http.Request and provides it in the Body type.
Functions ¶
func BadRequest ¶
BadRequest annotates an error has giving 400 HTTP return code
func GenerateDecoder ¶
func GenerateDecoder( genOpts ...DecodeInputsGeneratorOpt, ) interface{}
GenerateDecoder injects a special provider that uses nject.GenerateFromInjectionChain to examine the injection chain to see if there are any models that are used but never provided. If so, it looks at the struct tags in the models to see if they are tagged for filling with the decoder. If so, a provider is created that injects the missing model into the dependency chain. The intended use for this is to have an endpoint handler receive the deocded request body.
Major warning: the endpoint handler must receive the request model as a field inside a model, not as a standalone model.
The following tags are recognized:
`nvelope:"model"` causes the POST or PUT body to be decoded using a decoder like json.Unmarshal.
`nvelope:"path,name=xxx"` causes part of the URL path to be extracted and written to the tagged field.
`nvelope:"query,name=xxx"` causes the named URL query parameters to be extracted and written to the tagged field.
`nvelope:"header,name=xxx"` causes the named HTTP header to be extracted and written to the tagged field.
`nvelope:"cookie,name=xxx"` cause the named HTTP cookie to be extracted and writted to the tagged field.
Path, query, header, and cookie support options described in https://swagger.io/docs/specification/serialization/ for controlling how to serialize. The following are supported as appropriate.
explode=true # default for query, header explode=false # default for path delimiter=comma # default delimiter=space # query parameters only delimiter=pipe # query parameters only allowReserved=false # default allowReserved=true # query parameters only form=false # default form=true # query paramters only, may extract value from application/x-www-form-urlencoded POST content formOnly=false # default formOnly=true # query paramters only, extract value from application/x-www-form-urlencoded POST content only content=application/json # specifies that the value should be decoded with JSON content=application/xml # specifies that the value should be decoded with XML content=application/yaml # specifies that the value should be decoded with YAML content=text/yaml # specifies that the value should be decoded with YAML deepObject=false # default deepObject=true # required for query object
"style=label" and "style=matrix" are NOT yet supported for path parameters.
For query parameters filling maps and structs, the only the following combinations are supported:
deepObject=true deepObject=false,explode=false
When filling embedded structs from query, or header, parameters, using explode=false or deepObject=true, tagging struct members is optional. Tag them with their name or with "-" if you do not want them filled.
type Fillme struct { Embedded struct { IntValue int // will get filled by key "IntValue" FloatValue float64 `nvelope:"-"` // will not get filled StringValue string `nvelope:"bob"` // will get filled by key "bob" } `nvelope:"query,name=embedded,explode=false"` }
"deepObject=true" is only supported for maps and structs and only for query parameters.
Use "explode=true" combined with setting a "content" when you have a map to a struct or a slice of structs and each value will be encoded in JSON/XML independently. If the entire map is encoded, then use "explode=false".
GenerateDecoder uses https://pkg.go.dev/github.com/muir/reflectutils#MakeStringSetter to unpack strings into struct fields. That provides support for time.Duration and anything that implements encoding.TextUnmarshaler or flag.Value. Additional custom decoders can be registered with https://pkg.go.dev/github.com/muir/reflectutils#RegisterStringSetter .
There are a couple of example decoders defined in https://github.com/muir/nape and also https://github.com/muir/nchi .
func GetReturnCode ¶
GetReturnCode turns an error into an HTTP response code.
func MakeResponseEncoder ¶
func MakeResponseEncoder( name string, encoderFuncArgs ...ResponseEncoderFuncArg, ) nject.Provider
MakeResponseEncoder generates an nject Provider to encode API responses.
The generated provider is a wrapper that invokes the rest of the handler injection chain and expect to receive as return values an Response and and error. If the error is not nil, then the response becomes the error.
If more than one encoder is configurured, then MakeResponseEncoder will default to the first one specified in its functional arguments.
func MiddlewareBaseWriter ¶
func MiddlewareBaseWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider
MiddlewareBaseWriter acts as a translator. In the Go world, there are a bunch of packages that expect to use the wrapping
func(http.HandlerFunc) http.HandlerFunc
pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to use and not as expressive as the patterns supported by npoint and nvelope, but there may be code written with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with npoint and nvelope.
MiddlewareBaseWriter converts existing func(http.HandlerFunc) http.HandlerFunc functions so that they're compatible with nject. Because Middleware may wrap http.ResponseWriter, it should be used earlier in the injection chain than InjectWriter so that InjectWriter gets the already-wrapped http.ResponseWriter. Use MiddlewareBaseWriter if you suspect that the middleware you're wrapping replaces the writer.
func MiddlewareDeferredWriter ¶
func MiddlewareDeferredWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider
MiddlewareDeferredWriter acts as a translator. In the Go world, there are a bunch of packages that expect to use the wrapping
func(http.HandlerFunc) http.HandlerFunc
pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to use and not as expressive as the patterns supported by npoint and nvelope, but there may be code written with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with npoint and nvelope.
MiddlewareDeferredWriter converts existing func(http.HandlerFunc) http.HandlerFunc functions so that they're compatible with nject. MiddlewareDeferredWriter injects a DeferredWriter into the the func(http.HandlerFunc) http.HandlerFunc handler chain. If the chain replaces the writer, there will be two writers in play at once and results may be inconsistent. MiddlewareDeferredWriter must be used after InjectWriter. Use MiddlewareDeferredWriter if you know that the middleware you're wrapping does not replace the writer.
func MiddlewareHandlerBaseWriter ¶
MiddlewareHandlerBaseWriter acts as a translator. In the Go world, there are a bunch of packages that expect to use the wrapping
func(http.Handler) http.Handler
pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to use and not as expressive as the patterns supported by npoint and nvelope, but there may be code written with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with npoint and nvelope.
MiddlewareHandlerBaseWriter converts existing func(http.Handler) http.Handler functions so that they're compatible with nject. Because Middleware may wrap http.ResponseWriter, it should be used earlier in the injection chain than InjectWriter so that InjectWriter gets the already-wrapped http.ResponseWriter. Use MiddlewareBaseWriter if you suspect that the middleware you're wrapping replaces the writer.
func MiddlewareHandlerDeferredWriter ¶
MiddlewareHandlerDeferredWriter acts as a translator. In the Go world, there are a bunch of packages that expect to use the wrapping
func(http.Handler) http.Handler
pattern. The func(http.Handler) http.Handler pattern is harder to use and not as expressive as the patterns supported by npoint and nvelope, but there may be code written with the func(http.Handler) http.Handler pattern that you want to use with npoint and nvelope.
MiddlewareHandlerDeferredWriter converts existing func(http.Handler) http.Handler functions so that they're compatible with nject. MiddlewareHandlerDeferredWriter injects a DeferredWriter into the the func(http.Handler) http.Handler handler chain. If the chain replaces the writer, there will be two writers in play at once and results may be inconsistent. MiddlewareHandlerDeferredWriter must be used after InjectWriter. Use MiddlewareHandlerDeferredWriter if you know that the middleware you're wrapping does not replace the writer.
func RecoverInterface ¶
func RecoverInterface(err error) interface{}
RecoverInterface returns the interface{} that recover() originally provided. Or it returns nil if the error isn't a from a panic recovery. This works only in conjunction with SetErrorOnPanic() and CatchPanic.
func RecoverStack ¶
RecoverStack returns the stack from when recover() originally caught the panic. Or it returns "" if the error isn't a from a panic recovery. This works only in conjunction with SetErrorOnPanic() and CatchPanic.
Example ¶
package main import ( "fmt" "github.com/muir/nvelope" ) func main() { f := func(i int) (err error) { defer nvelope.SetErrorOnPanic(&err, nvelope.NoLogger()) return func() error { switch i { case 0: panic("zero") case 1: return fmt.Errorf("a one") default: return nil } }() } err := f(0) fmt.Println(err) stack := nvelope.RecoverStack(err) fmt.Println(len(stack) > 1000) }
Output: panic: zero true
func ReturnCode ¶
ReturnCode associates an HTTP return code with a error. if err is nil, then nil is returned.
func SetErrorOnPanic ¶
func SetErrorOnPanic(ep *error, log BasicLogger)
SetErrorOnPanic should be called as a defer. It sets an error value if there is a panic.
func Unauthorized ¶
Unauthorized annotates an error has giving 401 HTTP return code
Types ¶
type APIEnforcerFunc ¶
type BasicLogger ¶
type BasicLogger interface { Debug(msg string, fields ...map[string]interface{}) Error(msg string, fields ...map[string]interface{}) Warn(msg string, fields ...map[string]interface{}) }
BasicLogger is just the start of what a logger might support. It exists mostly as a placeholder. Future versions of nvelope will prefer more capabile loggers but will use type assertions so that the BasicLogger will remain acceptable to the APIs.
type Body ¶
type Body []byte
Body is a type provideded by ReadBody: it is a []byte with the request body pre-read.
type CanModel ¶
type CanModel interface { error Model() encoding.TextUnmarshaler }
CanModel represents errors that can transform themselves into a model for logging.
type DecodeInputsGeneratorOpt ¶
type DecodeInputsGeneratorOpt func(*eigo)
DecodeInputsGeneratorOpt are functional arguments for GenerateDecoder
func RejectUnknownQueryParameters ¶
func RejectUnknownQueryParameters(b bool) DecodeInputsGeneratorOpt
RejectUnknownQueryParameters true indicates that if there are any query parameters supplied that were not expected, the request should be rejected with a 400 response code. This parameter also controls what happens if there an embedded object is filled and there is no object key corresponding to the request parameter.
This does not apply to query parameters with content=application/json decodings. If you want to disallow unknown tags for content= decodings, define a custom decoder.
func WithDecoder ¶
func WithDecoder(contentType string, decoder Decoder) DecodeInputsGeneratorOpt
WithDecoder maps conent types (eg "application/json") to decode functions (eg json.Unmarshal). If a Content-Type header is used in the requet, then the value of that header will be used to pick a decoder.
When using a decoder, the body must be provided as an nvelope.Body parameter. Use nvelope.ReadBody to do that.
func WithDefaultContentType ¶
func WithDefaultContentType(contentType string) DecodeInputsGeneratorOpt
WithDefaultContentType specifies which model decoder to use when no "Content-Type" header was sent.
func WithPathVarsFunction ¶
func WithPathVarsFunction(pathVarFunction interface{}) DecodeInputsGeneratorOpt
WithPathVarsFunction is required if there are any variables from the path/route that need to be extracted. What's required is a function that returns a function to lookup path/route variables. The first function can take whatever arguments it needs and they'll be supplied as part of the injection chain.
For gorilla/mux:
WithPathVarsFunction(func(r *http.Request) RouteVarLookup { vars := mux.Vars(r) return func(v string) string { return vars[v] } })
For httprouter:
WithPathVarsFunction(func(params httprouter.Params) RouteVarLookup { return params.ByName })
func WithTag ¶
func WithTag(tag string) DecodeInputsGeneratorOpt
WithTag overrides the tag for specifying fields to be filled from the http request. The default is "nvelope"
type Decoder ¶
Decoder is the signature for decoders: take bytes and a pointer to something and deserialize it.
type DeferredWriter ¶
type DeferredWriter struct {
// contains filtered or unexported fields
}
DeferredWriter that wraps an underlying http.ResponseWriter. DeferredWriter buffers writes and headers. The buffer can be reset. When it's time to actually write, use Flush().
func NewDeferredWriter ¶
func NewDeferredWriter(w http.ResponseWriter) (*DeferredWriter, http.ResponseWriter)
NewDeferredWriter returns a DeferredWriter based on a base ResponseWriter. It re-injects the base writer so that in effect, there is only one writer present.
func (*DeferredWriter) Body ¶ added in v0.4.0
func (w *DeferredWriter) Body() ([]byte, int, error)
Body returns the internal buffer used by DeferredWriter. Do not modify it. It also returns the status code (if set). If UnderlyingWriter() has been called, then Body() will return an error since the underlying buffer does not represent what has been written.
func (*DeferredWriter) Done ¶
func (w *DeferredWriter) Done() bool
Done returns true if the DeferredWriter is in passthrough mode.
func (*DeferredWriter) Flush ¶
func (w *DeferredWriter) Flush() error
Flush pushes the buffered write content through to the base writer. You can only flush once. After a flush, all further calls are passed through to be base writer. WriteHeader() will be called on the base writer even if there is no buffered data.
func (*DeferredWriter) FlushIfNotFlushed ¶
func (w *DeferredWriter) FlushIfNotFlushed() error
FlushIfNotFlushed calls Flush if the DeferredWriter is not in passthrough mode.
func (*DeferredWriter) Header ¶
func (w *DeferredWriter) Header() http.Header
Header is the same as http.ResponseWriter.Header
func (*DeferredWriter) PreserveHeader ¶
func (w *DeferredWriter) PreserveHeader()
PreserveHeader saves the current Header so that a Reset will revert back to the header just saved.
func (*DeferredWriter) Reset ¶
func (w *DeferredWriter) Reset() error
Reset empties the DeferredWriter's buffers and resets its Header back to its original state. Reset returns error if UnderlyingWriter() or Flush() have been called.
func (*DeferredWriter) UnderlyingWriter ¶
func (w *DeferredWriter) UnderlyingWriter() http.ResponseWriter
UnderlyingWriter returns the underlying writer. Any header modifications made with the DeferredWriter are copied to the base writer. After a call to UnderlyingWriter, the DeferredWriter switches to passthrough mode: all future calls to Write(), Header(), etc are passed through to the http.ResponseWriter that was used to initialize the DeferredWrited.
Any writes made before the call to UnderlyingWriter are discarded. Call Flush() first to preserve writes.
func (*DeferredWriter) Write ¶
func (w *DeferredWriter) Write(b []byte) (int, error)
Write is the same as http.ResponseWriter.Write except that the action is delayed until Flush() is called.
func (*DeferredWriter) WriteHeader ¶
func (w *DeferredWriter) WriteHeader(statusCode int)
WriteHeader is the same as http.ResponseWriter.WriteHeader except that the action is delayed until Flush() is called.
type EncoderSpecificFuncArg ¶
type EncoderSpecificFuncArg func(*specificEncoder)
EncoderSpecificFuncArg is a functional arguemnt for WithEncoder
func WithAPIEnforcer ¶
func WithAPIEnforcer(apiEnforcer APIEnforcerFunc) EncoderSpecificFuncArg
WithAPIEnforcer specifies a function that can check if the encoded API response is valid for the endpoint that is generating the response. This is where swagger enforcement could be added. The default is not not verify API conformance.
https://github.com/muir/nvalid provides a function to generate an APIEnforcerFunc from swagger.
func WithEncoderErrorTransform ¶
func WithEncoderErrorTransform(errorTransformer ErrorTranformer) EncoderSpecificFuncArg
WithEncoderErrorTransform provides an encoder-specific function to transform errors before encoding them using the normal encoder. The return values are the model to use instead of the error and a boolean to indicate that the replacement should be used. If the boolean is false, then a plain text error message will be generated using err.Error().
type ErrorTranformer ¶
ErrorTranformer transforms an error into a model that can be logged.
type LogFlusher ¶
type LogFlusher interface {
Flush()
}
LogFlusher is used to check if a logger implements Flush(). This is useful as part of a panic handler.
type Response ¶
type Response interface{}
Response is an empty interface that is the expected return value from endpoints.
type ResponseEncoderFuncArg ¶
type ResponseEncoderFuncArg func(*encoderOptions)
ResponseEncoderFuncArg is a function argument for MakeResponseEncoder
func WithEncoder ¶
func WithEncoder(contentType string, encode func(interface{}) ([]byte, error), encoderOpts ...EncoderSpecificFuncArg) ResponseEncoderFuncArg
WithEncoder adds an model encoder to what MakeResponseEncoder will support. The first encoder added becomes the default encoder that is used if there is no match between the client's Accept header and the encoders that MakeResponseEncoder knows about.
func WithErrorModel ¶
func WithErrorModel(errorTransformer ErrorTranformer) ResponseEncoderFuncArg
WithErrorModel provides a function to transform errors before encoding them using the normal encoder. The return values are the model to use instead of the error and a boolean to indicate that the replacement should be used. If the boolean is false, then a plain text error message will be generated using err.Error().