Repository: goadesign/goa
Pull request #3513 (2024-04-24 19:18:34)

Handle unsupported request content type
Return 415 Unsupported Media Type when the request decoder does not support the request content type.


Patch:
diff --git a/http/codegen/templates/request_decoder.go.tpl b/http/codegen/templates/request_decoder.go.tpl
index a8b4581e..01136ccd 100644
--- a/http/codegen/templates/request_decoder.go.tpl
+++ b/http/codegen/templates/request_decoder.go.tpl
@@ -4,6 +4,10 @@ func {{ .RequestDecoder }}(mux goahttp.Muxer, decoder func(*http.Request) goahtt
 {{- if .MultipartRequestDecoder }}
 		var payload {{ .Payload.Ref }}
 		if err := decoder(r).Decode(&payload); err != nil {
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 {{- else if .Payload.Request.ServerBody }}
@@ -22,6 +26,10 @@ func {{ .RequestDecoder }}(mux goahttp.Muxer, decoder func(*http.Request) goahtt
 				err = nil
 			} else {
 	{{- end }}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 	{{- if not .Payload.Request.MustHaveBody }}
 			}
diff --git a/http/encoding.go b/http/encoding.go
index d207d93e..5a2947fb 100644
--- a/http/encoding.go
+++ b/http/encoding.go
@@ -11,6 +11,8 @@ import (
 	"mime"
 	"net/http"
 	"strings"
+
+	goa "goa.design/goa/v3/pkg"
 )
 
 const (
@@ -80,7 +82,7 @@ func RequestDecoder(r *http.Request) Decoder {
 	case "text/html", "text/plain":
 		return newTextDecoder(r.Body, contentType)
 	default:
-		return json.NewDecoder(r.Body)
+		return newUnsupportedDecoder(contentType)
 	}
 }
 
@@ -306,3 +308,17 @@ func (e *textDecoder) Decode(v any) error {
 	}
 	return nil
 }
+
+// newUnsupportedDecoder returns a decoder that returns an error indicating that
+// the content type is not supported.
+func newUnsupportedDecoder(ct string) Decoder {
+	return &unsupportedDecoder{ct}
+}
+
+type unsupportedDecoder struct {
+	ct string
+}
+
+func (e *unsupportedDecoder) Decode(_ any) error {
+	return goa.UnsupportedMediaTypeError(e.ct)
+}
diff --git a/http/error.go b/http/error.go
index 79f03b7c..d6457eff 100644
--- a/http/error.go
+++ b/http/error.go
@@ -81,6 +81,9 @@ func (resp *ErrorResponse) MarshalXML(e *xml.Encoder, _ xml.StartElement) error
 // error. This method is used by the generated server code when the error is not
 // described explicitly in the design.
 func (resp *ErrorResponse) StatusCode() int {
+	if resp.Name == goa.UnsupportedMediaType {
+		return http.StatusUnsupportedMediaType
+	}
 	if resp.Fault {
 		return http.StatusInternalServerError
 	}
diff --git a/pkg/error.go b/pkg/error.go
index 35a66b70..49e1ed3b 100644
--- a/pkg/error.go
+++ b/pkg/error.go
@@ -56,6 +56,9 @@ const (
 	InvalidRange = "invalid_range"
 	// InvalidLength is the error name for invalid length errors.
 	InvalidLength = "invalid_length"
+	// UnsupportedMediaType is the error name returned by the Goa decoder
+	// when the content type of the HTTP request body is not supported.
+	UnsupportedMediaType = "unsupported_media_type"
 )
 
 // NewServiceError creates an error.
@@ -116,6 +119,12 @@ func DecodePayloadError(msg string) error {
 	return PermanentError("decode_payload", msg)
 }
 
+// UnsupportedMediaTypeError is the error produced by the Goa decoder when the
+// content type of the HTTP request body is not supported.
+func UnsupportedMediaTypeError(ct string) error {
+	return PermanentError(UnsupportedMediaType, "unsupported media type %s", ct)
+}
+
 // InvalidFieldTypeError is the error produced by the generated code when the
 // type of a payload field does not match the type defined in the design.
 func InvalidFieldTypeError(name string, val any, expected string) error {


Test patch:
diff --git a/http/codegen/testdata/payload_decode_functions.go b/http/codegen/testdata/payload_decode_functions.go
index ca873407..35fbc65c 100644
--- a/http/codegen/testdata/payload_decode_functions.go
+++ b/http/codegen/testdata/payload_decode_functions.go
@@ -3728,6 +3728,10 @@ func DecodeMethodBodyStringRequest(mux goahttp.Muxer, decoder func(*http.Request
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyStringPayload(&body)
@@ -3750,6 +3754,10 @@ func DecodeMethodBodyStringValidateRequest(mux goahttp.Muxer, decoder func(*http
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyStringValidateRequestBody(&body)
@@ -3776,6 +3784,10 @@ func DecodeMethodBodyUserRequest(mux goahttp.Muxer, decoder func(*http.Request)
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyUserPayloadType(&body)
@@ -3798,6 +3810,10 @@ func DecodeMethodBodyUserRequest(mux goahttp.Muxer, decoder func(*http.Request)
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyUserRequestBody(&body)
@@ -3824,6 +3840,10 @@ func DecodeMethodBodyUserRequest(mux goahttp.Muxer, decoder func(*http.Request)
 			if err == io.EOF {
 				err = nil
 			} else {
+				var gerr *goa.ServiceError
+				if errors.As(err, &gerr) {
+					return nil, gerr
+				}
 				return nil, goa.DecodePayloadError(err.Error())
 			}
 		}
@@ -3851,6 +3871,10 @@ func DecodeMethodBodyUserValidateRequest(mux goahttp.Muxer, decoder func(*http.R
 			if err == io.EOF {
 				err = nil
 			} else {
+				var gerr *goa.ServiceError
+				if errors.As(err, &gerr) {
+					return nil, gerr
+				}
 				return nil, goa.DecodePayloadError(err.Error())
 			}
 		}
@@ -3880,6 +3904,10 @@ func DecodeMethodBodyObjectRequest(mux goahttp.Muxer, decoder func(*http.Request
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyObjectPayload(body)
@@ -3904,6 +3932,10 @@ func DecodeMethodBodyObjectValidateRequest(mux goahttp.Muxer, decoder func(*http
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyObjectValidatePayload(body)
@@ -3926,6 +3958,10 @@ func DecodeMethodBodyUnionRequest(mux goahttp.Muxer, decoder func(*http.Request)
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyUnionRequestBody(&body)
@@ -3952,6 +3988,10 @@ func DecodeMethodBodyUnionValidateRequest(mux goahttp.Muxer, decoder func(*http.
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyUnionValidateRequestBody(&body)
@@ -3978,6 +4018,10 @@ func DecodeMethodBodyUnionUserRequest(mux goahttp.Muxer, decoder func(*http.Requ
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyUnionUserRequestBody(&body)
@@ -4004,6 +4048,10 @@ func DecodeMethodBodyUnionUserValidateRequest(mux goahttp.Muxer, decoder func(*h
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyUnionUserValidateRequestBody(&body)
@@ -4030,6 +4078,10 @@ func DecodeMethodBodyArrayStringRequest(mux goahttp.Muxer, decoder func(*http.Re
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyArrayStringPayload(&body)
@@ -4053,6 +4105,10 @@ func DecodeMethodBodyArrayStringValidateRequest(mux goahttp.Muxer, decoder func(
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyArrayStringValidateRequestBody(&body)
@@ -4079,6 +4135,10 @@ func DecodeMethodBodyArrayUserRequest(mux goahttp.Muxer, decoder func(*http.Requ
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyArrayUserRequestBody(&body)
@@ -4105,6 +4165,10 @@ func DecodeMethodBodyArrayUserValidateRequest(mux goahttp.Muxer, decoder func(*h
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyArrayUserValidateRequestBody(&body)
@@ -4131,6 +4195,10 @@ func DecodeMethodBodyMapStringRequest(mux goahttp.Muxer, decoder func(*http.Requ
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyMapStringPayload(&body)
@@ -4153,6 +4221,10 @@ func DecodeMethodBodyMapStringValidateRequest(mux goahttp.Muxer, decoder func(*h
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyMapStringValidateRequestBody(&body)
@@ -4179,6 +4251,10 @@ func DecodeMethodBodyMapUserRequest(mux goahttp.Muxer, decoder func(*http.Reques
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyMapUserRequestBody(&body)
@@ -4205,6 +4281,10 @@ func DecodeMethodBodyMapUserValidateRequest(mux goahttp.Muxer, decoder func(*htt
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyMapUserValidateRequestBody(&body)
@@ -4232,6 +4312,10 @@ func DecodeMethodBodyPrimitiveStringValidateRequest(mux goahttp.Muxer, decoder f
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if !(body == "val") {
@@ -4261,6 +4345,10 @@ func DecodeMethodBodyPrimitiveBoolValidateRequest(mux goahttp.Muxer, decoder fun
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if !(body == true) {
@@ -4290,6 +4378,10 @@ func DecodeMethodBodyPrimitiveArrayStringValidateRequest(mux goahttp.Muxer, deco
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if len(body) < 1 {
@@ -4324,6 +4416,10 @@ func DecodeMethodBodyPrimitiveArrayBoolValidateRequest(mux goahttp.Muxer, decode
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if len(body) < 1 {
@@ -4358,6 +4454,10 @@ func DecodeMethodBodyPrimitiveArrayUserValidateRequest(mux goahttp.Muxer, decode
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if len(body) < 1 {
@@ -4394,6 +4494,10 @@ func DecodeMethodBodyPrimitiveArrayUserRequest(mux goahttp.Muxer, decoder func(*
 			if err == io.EOF {
 				err = nil
 			} else {
+				var gerr *goa.ServiceError
+				if errors.As(err, &gerr) {
+					return nil, gerr
+				}
 				return nil, goa.DecodePayloadError(err.Error())
 			}
 		}
@@ -4418,6 +4522,10 @@ func DecodeMethodBodyPrimitiveArrayUserRequest(mux goahttp.Muxer, decoder func(*
 			if err == io.EOF {
 				err = nil
 			} else {
+				var gerr *goa.ServiceError
+				if errors.As(err, &gerr) {
+					return nil, gerr
+				}
 				return nil, goa.DecodePayloadError(err.Error())
 			}
 		}
@@ -4442,6 +4550,10 @@ func DecodeMethodBodyPrimitiveArrayUserValidateRequest(mux goahttp.Muxer, decode
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		if len(body) < 1 {
@@ -4473,6 +4585,10 @@ func DecodeMethodBodyQueryObjectRequest(mux goahttp.Muxer, decoder func(*http.Re
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4504,6 +4620,10 @@ func DecodeMethodBodyQueryObjectValidateRequest(mux goahttp.Muxer, decoder func(
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyQueryObjectValidateRequestBody(&body)
@@ -4542,6 +4662,10 @@ func DecodeMethodBodyQueryUserRequest(mux goahttp.Muxer, decoder func(*http.Requ
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4572,6 +4696,10 @@ func DecodeMethodBodyQueryUserValidateRequest(mux goahttp.Muxer, decoder func(*h
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyQueryUserValidateRequestBody(&body)
@@ -4610,6 +4738,10 @@ func DecodeMethodBodyPathObjectRequest(mux goahttp.Muxer, decoder func(*http.Req
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4640,6 +4772,10 @@ func DecodeMethodBodyPathObjectValidateRequest(mux goahttp.Muxer, decoder func(*
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyPathObjectValidateRequestBody(&body)
@@ -4677,6 +4813,10 @@ func DecodeMethodBodyPathUserRequest(mux goahttp.Muxer, decoder func(*http.Reque
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4706,6 +4846,10 @@ func DecodeMethodUserBodyPathValidateRequest(mux goahttp.Muxer, decoder func(*ht
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodUserBodyPathValidateRequestBody(&body)
@@ -4743,6 +4887,10 @@ func DecodeMethodBodyQueryPathObjectRequest(mux goahttp.Muxer, decoder func(*htt
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4778,6 +4926,10 @@ func DecodeMethodBodyQueryPathObjectValidateRequest(mux goahttp.Muxer, decoder f
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyQueryPathObjectValidateRequestBody(&body)
@@ -4821,6 +4973,10 @@ func DecodeMethodBodyQueryPathUserRequest(mux goahttp.Muxer, decoder func(*http.
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -4856,6 +5012,10 @@ func DecodeMethodBodyQueryPathUserValidateRequest(mux goahttp.Muxer, decoder fun
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodBodyQueryPathUserValidateRequestBody(&body)
@@ -4994,6 +5154,10 @@ func DecodeMethodMapQueryObjectRequest(mux goahttp.Muxer, decoder func(*http.Req
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		err = ValidateMethodMapQueryObjectRequestBody(&body)
@@ -5043,6 +5207,10 @@ func DecodeMethodMultipartPrimitiveRequest(mux goahttp.Muxer, decoder func(*http
 	return func(r *http.Request) (any, error) {
 		var payload string
 		if err := decoder(r).Decode(&payload); err != nil {
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -5057,6 +5225,10 @@ func DecodeMethodMultipartUserTypeRequest(mux goahttp.Muxer, decoder func(*http.
 	return func(r *http.Request) (any, error) {
 		var payload *servicemultipartusertype.MethodMultipartUserTypePayload
 		if err := decoder(r).Decode(&payload); err != nil {
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -5071,6 +5243,10 @@ func DecodeMethodMultipartArrayTypeRequest(mux goahttp.Muxer, decoder func(*http
 	return func(r *http.Request) (any, error) {
 		var payload []*servicemultipartarraytype.PayloadType
 		if err := decoder(r).Decode(&payload); err != nil {
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -5085,6 +5261,10 @@ func DecodeMethodMultipartMapTypeRequest(mux goahttp.Muxer, decoder func(*http.R
 	return func(r *http.Request) (any, error) {
 		var payload map[string]int
 		if err := decoder(r).Decode(&payload); err != nil {
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -5106,6 +5286,10 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 
@@ -5849,6 +6033,10 @@ func DecodeMethodBodyCustomNameRequest(mux goahttp.Muxer, decoder func(*http.Req
 			if err == io.EOF {
 				return nil, goa.MissingPayloadError()
 			}
+			var gerr *goa.ServiceError
+			if errors.As(err, &gerr) {
+				return nil, gerr
+			}
 			return nil, goa.DecodePayloadError(err.Error())
 		}
 		payload := NewMethodBodyCustomNamePayload(&body)
diff --git a/http/encoding_test.go b/http/encoding_test.go
index caf701cd..76b77f6a 100644
--- a/http/encoding_test.go
+++ b/http/encoding_test.go
@@ -11,6 +11,9 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	goa "goa.design/goa/v3/pkg"
 )
 
@@ -36,25 +39,70 @@ func TestRequestEncoder(t *testing.T) {
 	}
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
-			r := &http.Request{
-				Header: http.Header{},
-			}
+			r := &http.Request{Header: http.Header{}}
 			if c.requestCT != "" {
 				r.Header.Set(ct, c.requestCT)
 			}
 
 			encoder := RequestEncoder(r)
 
-			if gotT := fmt.Sprintf("%T", encoder); gotT != wantT {
-				t.Errorf("got encoder type %s, want %s", gotT, wantT)
-			}
-			if gotCT := r.Header.Get(ct); gotCT != c.wantCT {
-				t.Errorf("got Content-Type %q, want %q", gotCT, c.wantCT)
+			assert.Equal(t, wantT, fmt.Sprintf("%T", encoder))
+			assert.Equal(t, c.wantCT, r.Header.Get(ct))
+		})
+	}
+}
+
+func TestRequestDecoder(t *testing.T) {
+	const (
+		ct           = "Content-Type"
+		ctJSON       = "application/json"
+		ctXML        = "application/xml"
+		ctGob        = "application/gob"
+		unsupportedT = "*http.unsupportedDecoder"
+		jsonT        = "*json.Decoder"
+		xmlT         = "*xml.Decoder"
+		gobT         = "*gob.Decoder"
+	)
+	cases := []struct {
+		name      string
+		requestCT string
+		wantCT    string
+	}{
+		{"no ct", "", jsonT},
+		{"unsupported ct", "application/foo", unsupportedT},
+		{"json ct", ctJSON, jsonT},
+		{"xml ct", ctXML, xmlT},
+		{"gob ct", ctGob, gobT},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			r := &http.Request{Header: http.Header{}}
+			if c.requestCT != "" {
+				r.Header.Set(ct, c.requestCT)
 			}
+
+			decoder := RequestDecoder(r)
+
+			assert.Equal(t, c.wantCT, fmt.Sprintf("%T", decoder))
 		})
 	}
 }
 
+func TestUnsupportedDecoder(t *testing.T) {
+	// Write the response produced when writing the error returned the
+	// unsupported decoder to validate the response status code.
+	w := httptest.NewRecorder()
+	decoder := &unsupportedDecoder{"application/foo"}
+	err := decoder.Decode(nil)
+	require.Error(t, err)
+	encoder := ErrorEncoder(ResponseEncoder, nil)
+
+	err = encoder(context.Background(), w, err)
+
+	require.NoError(t, err)
+	assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
+}
+
 func TestResponseEncoder(t *testing.T) {
 	cases := []struct {
 		name        string
@@ -101,10 +149,10 @@ func TestResponseEncoder(t *testing.T) {
 			ctx = context.WithValue(ctx, AcceptTypeKey, c.acceptType)
 			ctx = context.WithValue(ctx, ContentTypeKey, c.contentType)
 			w := httptest.NewRecorder()
+
 			encoder := ResponseEncoder(ctx, w)
-			if c.encoderType != fmt.Sprintf("%T", encoder) {
-				t.Errorf("got encoder type %s, expected %s", fmt.Sprintf("%T", encoder), c.encoderType)
-			}
+
+			assert.Equal(t, c.encoderType, fmt.Sprintf("%T", encoder))
 		})
 	}
 }
@@ -138,13 +186,12 @@ func TestResponseEncoder_Encode_ErrorResponse(t *testing.T) {
 			w := httptest.NewRecorder()
 			ErrorResponseXMLName = c.xmlName
 			encoder := ResponseEncoder(ctx, w)
-			if err := encoder.Encode(NewErrorResponse(ctx, serviceError)); err != nil {
-				t.Error(err)
-			}
+
+			err := encoder.Encode(NewErrorResponse(ctx, serviceError))
+
+			assert.NoError(t, err)
 			body := strings.TrimSpace(w.Body.String())
-			if body != c.encoded {
-				t.Errorf("got %s, expected %s", body, c.encoded)
-			}
+			assert.Equal(t, c.encoded, body)
 		})
 	}
 }
@@ -184,9 +231,8 @@ func TestResponseDecoder(t *testing.T) {
 				},
 			}
 			decoder := ResponseDecoder(r)
-			if c.decoderType != fmt.Sprintf("%T", decoder) {
-				t.Errorf("got decoder type %s, expected %s", fmt.Sprintf("%T", decoder), c.decoderType)
-			}
+
+			assert.Equal(t, c.decoderType, fmt.Sprintf("%T", decoder))
 		})
 	}
 }
@@ -211,57 +257,43 @@ func TestTextEncoder_Encode(t *testing.T) {
 			buffer.Reset()
 			err := encoder.Encode(c.value)
 			if c.error != nil {
-				if err == nil || c.error.Error() != err.Error() {
-					t.Errorf("got error %q, expected %q", err, c.error)
-				}
-			} else {
-				if err != nil {
-					t.Errorf("got error %q, expected <nil>", err)
-				}
-				if buffer.String() != testString {
-					t.Errorf("got string %s, expected %s", buffer.String(), testString)
-				}
+				assert.Error(t, err, c.error)
+				return
 			}
+			require.NoError(t, err)
+			assert.Equal(t, testString, buffer.String())
 		})
 	}
 }
 
 func TestTextPlainDecoder_Decode_String(t *testing.T) {
 	decoder := makeTextDecoder()
-
 	var value string
+
 	err := decoder.Decode(&value)
-	if err != nil {
-		t.Errorf("got error %q, expected <nil>", err)
-	}
-	if testString != value {
-		t.Errorf("got string %s, expected %s", value, testString)
-	}
+
+	assert.NoError(t, err)
+	assert.Equal(t, testString, value)
 }
 
 func TestTextPlainDecoder_Decode_Bytes(t *testing.T) {
 	decoder := makeTextDecoder()
-
 	var value []byte
+
 	err := decoder.Decode(&value)
-	if err != nil {
-		t.Errorf("got error %q, expected <nil>", err)
-	}
-	if testString != string(value) {
-		t.Errorf("got string %s, expected %s", value, testString)
-	}
+
+	assert.NoError(t, err)
+	assert.Equal(t, testString, string(value))
 }
 
 func TestTextPlainDecoder_Decode_Other(t *testing.T) {
 	decoder := makeTextDecoder()
-
 	expectedErr := fmt.Errorf("can't decode content/type to *int")
-
 	var value int
+
 	err := decoder.Decode(&value)
-	if err == nil || err.Error() != expectedErr.Error() {
-		t.Errorf("got error %q, expectedErr %q", err, expectedErr)
-	}
+
+	assert.Error(t, err, expectedErr)
 }
 
 func makeTextDecoder() Decoder {