Content negotiation allows your HTTP services to support multiple content types and formats. Goa provides a flexible encoding and decoding strategy that makes it possible to associate arbitrary encoders and decoders with HTTP response and request content types.
The generated HTTP server constructor accepts encoder and decoder functions as arguments, allowing for custom implementations:
// New instantiates HTTP handlers for all the service endpoints
func New(
e *divider.Endpoints,
mux goahttp.Muxer,
decoder func(*http.Request) goahttp.Decoder,
encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
errhandler func(context.Context, http.ResponseWriter, error),
formatter func(context.Context, err error) goahttp.Statuser,
) *Server
The default Goa encoder and decoder are provided by the Goa http
package and
can be used like this:
import (
// ...
"goa.design/goa/v3/http"
)
// ...
server := calcsvr.New(endpoints, mux, http.RequestDecoder, http.ResponseEncoder, nil, nil)
Content type support in Goa determines how data is serialized and deserialized across the network boundary. The encoding and decoding roles switch between client and server, in order of execution:
The default Goa encoders and decoders support several common content types. These include:
application/json
, *+json
)application/xml
, *+xml
)application/gob
, *+gob
)text/html
)text/plain
)The suffix matching pattern allows for content type variants, such as
application/ld+json
, application/hal+json
, and application/vnd.api+json
.
The default response encoder implements a content negotiation strategy that considers multiple factors in sequence:
Accept
header of the incoming request to determine
the client’s preferred content types.Content-Type
header of the request if the Accept
header isn’t present.On the server side, the encoder processes the client’s Accept
header to
determine content type preferences. It then selects the most appropriate encoder
based on the available supported types. When no suitable match is found among
the accepted types, the encoder defaults to using JSON.
For client-side operations, the decoder processes the received response based on the content type specified in the response headers. When encountering unknown content types, it safely falls back to JSON decoding to maintain compatibility.
Request content type handling follows a simpler negotiation process than
responses. The process primarily relies on the Content-Type
header of the
request, with a fallback to a default content type when necessary.
On the server side, the decoder begins by inspecting the request’s
Content-Type
header. Based on this value, it selects the appropriate decoder
implementation—whether that’s JSON, XML, or gob. In cases where the
Content-Type
header is missing or specifies an unsupported format, the decoder
defaults to JSON to ensure request processing can continue.
For client-side operations, the encoder sets the content type based on the request configuration and encodes the request body accordingly. When no specific content type is provided, it defaults to JSON encoding to maintain consistent behavior.
In all cases, if encoding or decoding fails, Goa invokes the error handler that was registered during the HTTP server’s creation, allowing for graceful error handling and appropriate client feedback.
Use the ContentType
DSL to specify a default response content type:
var _ = Service("media", func() {
Method("create", func() {
HTTP(func() {
POST("/media")
Response(StatusCreated, func() {
// Override response content type
ContentType("application/json")
})
})
})
})
When set, this overrides any content type specified in request headers, but not the Accept
header value.
When Goa’s built-in encoders don’t meet your needs, you can implement custom encoders and decoders. You might need custom encoders to support specialized formats like MessagePack or BSON that aren’t included in Goa’s defaults. They’re also useful when you need to optimize encoding performance for your specific use case, add compression or encryption layers to your responses, or maintain compatibility with legacy or proprietary formats used by existing systems.
An encoder must implement the Encoder
interface defined in the Goa http
package and provide a constructor function:
// Encoder interface for response encoding
type Encoder interface {
Encode(v any) error
}
// Constructor function
func NewMessagePackEncoder(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
return &MessagePackEncoder{w: w}
}
The constructor function must return an Encoder
and an error:
// Constructor signature
func(ctx context.Context, w http.ResponseWriter) (goahttp.Encoder, error)
// Example MessagePack encoder
type MessagePackEncoder struct {
w http.ResponseWriter
}
func (enc *MessagePackEncoder) Encode(v interface{}) error {
// Set content type header
enc.w.Header().Set("Content-Type", "application/msgpack")
// Use MessagePack encoding
return msgpack.NewEncoder(enc.w).Encode(v)
}
// Constructor function
func NewMessagePackEncoder(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
return &MessagePackEncoder{w: w}
}
The context contains both ContentTypeKey
and AcceptTypeKey
values, allowing for content type negotiation.
A decoder must implement the goahttp.Decoder
interface and provide a constructor function:
// Constructor signature
func(r *http.Request) (goahttp.Decoder, error)
// Example MessagePack decoder
type MessagePackDecoder struct {
r *http.Request
}
func (dec *MessagePackDecoder) Decode(v interface{}) error {
return msgpack.NewDecoder(dec.r.Body).Decode(v)
}
// Constructor function
func NewMessagePackDecoder(r *http.Request) goahttp.Decoder {
return &MessagePackDecoder{r: r}
}
The constructor has access to the request object and can inspect its state to determine the appropriate decoder.
Use your custom encoder/decoder when creating the HTTP server:
func main() {
// Create endpoints
endpoints := myapi.NewEndpoints(svc)
// Create decoder factory
decoder := func(r *http.Request) goahttp.Decoder {
switch r.Header.Get("Content-Type") {
case "application/msgpack":
return NewMessagePackDecoder(r)
default:
return goahttp.RequestDecoder(r) // Default Goa decoder
}
}
// Create encoder factory
encoder := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
if accept := ctx.Value(goahttp.AcceptTypeKey).(string); accept == "application/msgpack" {
return NewMessagePackEncoder(ctx, w)
}
return goahttp.ResponseEncoder(ctx, w) // Default Goa encoder
}
// Create HTTP server with custom encoder/decoder
server := myapi.NewServer(endpoints, mux, decoder, encoder, nil, nil)
}
Error Handling
Performance
Headers
Content Negotiation
Goa automatically handles Accept header processing:
Example Accept header processing:
Accept: application/json;q=0.9, application/xml;q=0.8
Goa will:
Use content types for API versioning:
var _ = Service("versioned", func() {
HTTP(func() {
Response(func() {
ContentType(
"application/vnd.api.v1+json",
"application/vnd.api.v2+json"
)
})
})
})
Testing custom encoders and decoders requires careful validation of content type handling and negotiation. Here’s how to effectively test content negotiation using Clue’s mock package:
// Import Clue's mock package
import (
"github.com/goadesign/clue/mock"
)
// Mock encoder implementation using Clue's mock package
// This shows how to properly structure a mock encoder using Clue
type mockEncoder struct {
*mock.Mock // Embed Clue's Mock type
}
// Encode implements the mock using Clue's Next pattern
func (m *mockEncoder) Encode(v interface{}) error {
if f := m.Next("Encode"); f != nil {
return f.(func(interface{}) error)(v)
}
return errors.New("unexpected call to Encode")
}
// Mock decoder implementation using Clue's mock package
// This shows how to properly structure a mock decoder using Clue
type mockDecoder struct {
*mock.Mock // Embed Clue's Mock type
}
// Decode implements the mock using Clue's Next pattern
func (m *mockDecoder) Decode(v interface{}) error {
if f := m.Next("Decode"); f != nil {
return f.(func(interface{}) error)(v)
}
return errors.New("unexpected call to Decode")
}
func TestContentNegotiation(t *testing.T) {
// Create mock encoder and decoder using Clue's mock package
encoder := &mockEncoder{mock.New()}
decoder := &mockDecoder{mock.New()}
tests := []struct {
name string
contentType string
accept string
setupEncoder func(*mockEncoder)
setupDecoder func(*mockDecoder)
input interface{}
wantErr bool
}{
{
name: "JSON content type",
contentType: "application/json",
accept: "application/json",
setupEncoder: func(e *mockEncoder) {
// Use Clue's Set method for permanent behavior
e.Set("Encode", func(v interface{}) error {
// Verify JSON encoding
_, ok := v.(json.Marshaler)
if !ok {
return fmt.Errorf("value does not implement json.Marshaler")
}
return nil
})
},
setupDecoder: func(d *mockDecoder) {
// Use Clue's Set method for permanent behavior
d.Set("Decode", func(v interface{}) error {
// Verify JSON decoding
_, ok := v.(json.Unmarshaler)
if !ok {
return fmt.Errorf("value does not implement json.Unmarshaler")
}
return nil
})
},
input: &TestStruct{ID: "test"},
wantErr: false,
},
{
name: "MessagePack content type",
contentType: "application/msgpack",
accept: "application/msgpack",
setupEncoder: func(e *mockEncoder) {
// Use Clue's Add method for sequence-specific behavior
e.Add("Encode", func(v interface{}) error {
// Verify MessagePack encoding
_, ok := v.(msgpack.Marshaler)
if !ok {
return fmt.Errorf("value does not implement msgpack.Marshaler")
}
return nil
})
},
setupDecoder: func(d *mockDecoder) {
// Use Clue's Add method for sequence-specific behavior
d.Add("Decode", func(v interface{}) error {
// Verify MessagePack decoding
_, ok := v.(msgpack.Unmarshaler)
if !ok {
return fmt.Errorf("value does not implement msgpack.Unmarshaler")
}
return nil
})
},
input: &TestStruct{ID: "test"},
wantErr: false,
},
{
name: "unsupported content type",
contentType: "application/unknown",
accept: "application/unknown",
setupEncoder: func(e *mockEncoder) {
// Demonstrate error handling with Clue mocks
e.Set("Encode", func(v interface{}) error {
return fmt.Errorf("unsupported content type")
})
},
setupDecoder: func(d *mockDecoder) {
// Demonstrate error handling with Clue mocks
d.Set("Decode", func(v interface{}) error {
return fmt.Errorf("unsupported content type")
})
},
input: &TestStruct{ID: "test"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fresh Clue mocks for each test case
encoder := &mockEncoder{mock.New()}
decoder := &mockDecoder{mock.New()}
if tt.setupEncoder != nil {
tt.setupEncoder(encoder)
}
if tt.setupDecoder != nil {
tt.setupDecoder(decoder)
}
// Create request with content type
req := httptest.NewRequest("POST", "/", nil)
req.Header.Set("Content-Type", tt.contentType)
req.Header.Set("Accept", tt.accept)
// Create response recorder
rec := httptest.NewRecorder()
// Create encoder function
encoderFn := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
return encoder
}
// Create decoder function
decoderFn := func(r *http.Request) goahttp.Decoder {
return decoder
}
// Create handler with custom encoder/decoder
handler := goahttp.RequestDecoder(decoderFn)
// Test request decoding
err := handler(req, tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("RequestDecoder() error = %v, wantErr %v", err, tt.wantErr)
}
// Use Clue's HasMore method to verify all expected calls were made
if encoder.HasMore() {
t.Error("not all expected encoder operations were performed")
}
if decoder.HasMore() {
t.Error("not all expected decoder operations were performed")
}
})
}
}
This example demonstrates several key features of Clue’s mock package:
mockEncoder
and mockDecoder
) provide type-safe interfaces by embedding Clue’s Mock
type.Set
for permanent behaviors (see JSON content type test)Add
for sequence-specific behaviors (see MessagePack content type test)HasMore
method ensures all expected operations were performedThe test cases provide thorough coverage of essential content negotiation scenarios. They validate JSON content negotiation using permanent mock behaviors, ensuring consistent handling of this common format. The tests also verify MessagePack content negotiation through sequence-specific behaviors, demonstrating support for binary formats. Error conditions are tested by validating proper handling of unsupported content types. Finally, the test suite ensures proper cleanup by verifying that all mock expectations are met during test execution.
The tests verify: