Skip to main content

Echo Content Negotiation

Introduction

Content negotiation is a mechanism defined in the HTTP protocol that allows a client to specify which format of resource they prefer to receive. When a client sends a request to a server, it can include headers that indicate its preferred content type, language, encoding, etc. The server can then respond with the content in the format that best matches those preferences.

In the Echo framework, content negotiation allows your web applications to serve content in different formats (JSON, XML, HTML, etc.) based on what the client requests. This makes your APIs more flexible and user-friendly.

Understanding HTTP Content Negotiation

Before diving into Echo's implementation, let's understand how content negotiation works in HTTP:

  • Clients specify their preferences using Accept* headers
  • The primary headers are:
    • Accept: Preferred media types (e.g., application/json)
    • Accept-Language: Preferred languages (e.g., en-US)
    • Accept-Encoding: Preferred encoding (e.g., gzip, deflate)
    • Accept-Charset: Preferred character sets (e.g., UTF-8)

Echo's Content Negotiation Capabilities

Echo provides built-in support for content negotiation through its Context interface. The core method is Context.Negotiate(), which selects the most appropriate representation based on the client's Accept header.

Basic Content Negotiation Example

Here's a simple example showing how to implement content negotiation in Echo:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
)

type User struct {
Name string `json:"name" xml:"name"`
Email string `json:"email" xml:"email"`
}

func main() {
e := echo.New()

e.GET("/users/:id", func(c echo.Context) error {
// Get user data (in a real app, this would come from a database)
user := &User{
Name: "John Doe",
Email: "[email protected]",
}

// Respond with negotiated content type
return c.Negotiate(http.StatusOK, map[string]echo.HandlerFunc{
"application/json": func(c echo.Context) error {
return c.JSON(http.StatusOK, user)
},
"application/xml": func(c echo.Context) error {
return c.XML(http.StatusOK, user)
},
"text/html": func(c echo.Context) error {
return c.HTML(http.StatusOK, "<p>Name: John Doe<br>Email: [email protected]</p>")
},
})
})

e.Logger.Fatal(e.Start(":8080"))
}

How It Works

  1. We define a User struct with both JSON and XML tags.
  2. In our route handler, we create a user object with sample data.
  3. We call c.Negotiate() with:
    • An HTTP status code to return (200 OK)
    • A map of MIME types to handler functions

The Echo framework then:

  1. Examines the client's Accept header
  2. Finds the best matching content type from our provided options
  3. Calls the corresponding handler function
  4. If no suitable match is found, it returns 406 Not Acceptable

Testing Content Negotiation

You can test content negotiation using tools like cURL by setting the Accept header:

JSON Request

bash
curl -H "Accept: application/json" http://localhost:8080/users/1

Output:

json
{"name":"John Doe","email":"[email protected]"}

XML Request

bash
curl -H "Accept: application/xml" http://localhost:8080/users/1

Output:

xml
<User><name>John Doe</name><email>[email protected]</email></User>

HTML Request

bash
curl -H "Accept: text/html" http://localhost:8080/users/1

Output:

html
<p>Name: John Doe<br>Email: [email protected]</p>

Setting Default Content Type

You can specify a default content type for when the client doesn't provide an Accept header or when none of the client's preferred types is available:

go
func getUser(c echo.Context) error {
user := &User{
Name: "John Doe",
Email: "[email protected]",
}

return c.Negotiate(http.StatusOK, map[string]echo.HandlerFunc{
"application/json": func(c echo.Context) error {
return c.JSON(http.StatusOK, user)
},
"application/xml": func(c echo.Context) error {
return c.XML(http.StatusOK, user)
},
}, "application/json") // Default to JSON if no match
}

Advanced Content Negotiation Pattern

For larger applications, you might want to organize your content negotiation code more cleanly:

go
func getUserHandler(c echo.Context) error {
id := c.Param("id")

// Get user from database
user, err := getUserFromDB(id)
if err != nil {
return c.String(http.StatusNotFound, "User not found")
}

// Define responder functions
responders := map[string]echo.HandlerFunc{
"application/json": func(c echo.Context) error {
return c.JSON(http.StatusOK, user)
},
"application/xml": func(c echo.Context) error {
return c.XML(http.StatusOK, user)
},
"text/html": func(c echo.Context) error {
// In a real app, you might use a template engine here
html := fmt.Sprintf("<h1>User: %s</h1><p>Email: %s</p>",
user.Name, user.Email)
return c.HTML(http.StatusOK, html)
},
}

// Handle negotiation with JSON as default
return c.Negotiate(http.StatusOK, responders, "application/json")
}

func getUserFromDB(id string) (*User, error) {
// In a real app, fetch from database
// This is a mock implementation
return &User{
Name: "John Doe",
Email: "[email protected]",
}, nil
}

Real-world Use Case: API Versioning

Content negotiation can be used for API versioning. Clients can request specific API versions through content type:

go
func apiHandler(c echo.Context) error {
data := map[string]string{"message": "Hello, world!"}

return c.Negotiate(http.StatusOK, map[string]echo.HandlerFunc{
"application/vnd.myapp.v1+json": func(c echo.Context) error {
// Version 1 API response
return c.JSON(http.StatusOK, data)
},
"application/vnd.myapp.v2+json": func(c echo.Context) error {
// Version 2 API response with extra fields
enhancedData := map[string]interface{}{
"message": "Hello, world!",
"version": 2,
"timestamp": time.Now().Unix(),
}
return c.JSON(http.StatusOK, enhancedData)
},
}, "application/vnd.myapp.v1+json")
}

A client could request a specific version like:

bash
curl -H "Accept: application/vnd.myapp.v2+json" http://localhost:8080/api/resource

Best Practices for Content Negotiation

  1. Always provide a default format: Ensure your API works even without explicit Accept headers.
  2. Document supported formats: Make it clear which formats your API supports.
  3. Test all format combinations: Verify that each format works correctly.
  4. Use proper status codes: Return 406 Not Acceptable when appropriate.
  5. Consider performance implications: Some formats (like XML) may be more resource-intensive.

Summary

Echo's content negotiation features provide a powerful way to make your web applications and APIs more flexible. By responding with the client's preferred content type, you improve the user experience and make your application more interoperable with different clients.

Content negotiation is particularly valuable for:

  • RESTful APIs that need to support multiple formats
  • Applications that serve both human-readable (HTML) and machine-readable (JSON/XML) content
  • Supporting API versioning
  • Internationalization efforts

Additional Resources

Exercises

  1. Extend the user example to support an additional format like text/plain.
  2. Create an API endpoint that supports content negotiation for a list of items rather than a single item.
  3. Implement content negotiation that takes into account the Accept-Language header to serve content in different languages.
  4. Create an API that uses content negotiation to implement API versioning as shown in the advanced example.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)