Skip to content
Safigo
Go back

Accept header parser and matcher

Introduction

It’s not evident that the Accept header is an essential part of HTTP, especially REST communication. Usually, people do not worry about the Accept header, either I’m.

This short article will show how simple to add validation and matching to your HTTP router independently to the router and framework you use.

About Accept header

Accept header is a simple list of mime types (application/json, text/html, etc.) or wildcard rules (*/*, application/*, etc.) that a client supports. In the case of the provider Accept header, the client expects to respond with one of the matched mime types from the header.

This header also supports additional parameters that a client can provide per a mime type.

The main parameter is quality q, that defines order of rules to choose the best option for a client, for example: */*;q=0.1, application/json; q=1, application/xml; q=0.8. In this example, we prefer to receive a response in JSON. If it’s not supported, return in XML; otherwise, it does not matter the response mime type.

The second example is character encoding charset. If a client sets some charset, it expects to receive a response encoded.

Implementation notes

Following the description above. We can specify requirements for the Accept header parser

  1. Split all mime types by , symbol;
  2. Parse mime type and params;
  3. Order mime types and rules by:
    1. Use q parameter to sort;
    2. More strict mime types have more priority than wildcards;
    3. More parameters have more priority.
  4. Match supported mime types to Accept rules from the Accept header and find the best result for the client or server.

Creating middleware

Your router/framework can support this feature out of the box. If possible, do not spend time implementing your solution. However, read the article in case of:

Background

Header parsing explanation

In this block, we will review the main part of an example:

header := r.Header.Get("Accept")

// Parse Accept header to build needed rules for matching.
ah := mimeheader.ParseAcceptHeader(header)

// We do not need default mime type.
mh, mtype, m := ah.Negotiate(acceptMimeTypes, "")
if !m {
  // If not matched accept mim type, return 406.
  rw.WriteHeader(http.StatusNotAcceptable)

  return
}

// Add matched mime type to context.
ctx := context.WithValue(r.Context(), "resp_content_type", mtype)
// Add charset, if exists.
chs, ok := mh.Params["charset"]
if ok {
  ctx = context.WithValue(ctx, "resp_charset", chs)
}

Actually the main magic happenes in two lines of code:

// Parse Accept header to build needed rules for matching.
ah := mimeheader.ParseAcceptHeader(header)
// We do not need default mime type.
mh, mtype, m := ah.Negotiate(acceptMimeTypes, "")

That’s precisely the whole code needed to parse and match mime types. Other logic is related to the processing of retrieved data.

http/net middleware for http.HandleFunc

package main

import (
 "context"
 "log"
 "net/http"

 "github.com/safigo/mimeheader"
)

func main() {
 r := http.NewServeMux()

 r.HandleFunc("/", acceptHeaderMiddleware([]string{"application/json", "text/html"})(handlerTestFunc))

 err := http.ListenAndServe(":8080", r)
 if err != nil {
  log.Fatalln(err)
 }
}

func acceptHeaderMiddleware(acceptMimeTypes []string) func(http.HandlerFunc) http.HandlerFunc {
 return func(next http.HandlerFunc) http.HandlerFunc {
  return func(rw http.ResponseWriter, r *http.Request) {
   header := r.Header.Get("Accept")
   ah := mimeheader.ParseAcceptHeader(header)

   // We do not need default mime type.
   mh, mtype, m := ah.Negotiate(acceptMimeTypes, "")
   if !m {
    // If not matched accept mim type, return 406.
    rw.WriteHeader(http.StatusNotAcceptable)

    return
   }

   // Add matched mime type to context.
   ctx := context.WithValue(r.Context(), "resp_content_type", mtype)
   // Add charset, if set
   chs, ok := mh.Params["charset"]
   if ok {
    ctx = context.WithValue(ctx, "resp_charset", chs)
   }


   // New requet from new context.
   rc := r.WithContext(ctx)

   // Call next middleware or handler.
   next(rw, rc)
  }
 }
}

func handlerTestFunc(rw http.ResponseWriter, r *http.Request) {
 mtype := r.Context().Value("resp_content_type").(string)
 charset, _ := r.Context().Value("resp_charset").(string)

 rw.Write([]byte(mtype + ":" + charset))
}

Responses

GET http://localhost:8080/
Accept: text/*; q=0.9,application/json; q=1;

##HTTP/1.1 200 OK
##Date: Sat, 03 Jul 2021 23:55:41 GMT
##Content-Length: 17
##Content-Type: text/plain; charset=utf-8
##
##application/json:

####

GET http://localhost:8080/
Accept: text/*; q=1,application/json; q=1; charset=utf-8bm;

##HTTP/1.1 200 OK
##Date: Sat, 03 Jul 2021 23:56:14 GMT
##Content-Length: 24
##Content-Type: text/plain; charset=utf-8
##
##application/json:utf-8bm

####
GET http://localhost:8080/
Accept: text/html; charset=utf-8; q=1,application/*; q=1; charset=cp1251;

##HTTP/1.1 200 OK
##Date: Sat, 03 Jul 2021 23:54:20 GMT
##Content-Length: 14
##Content-Type: text/plain; charset=utf-8
##
##text/html:utf-8

####
GET http://localhost:8080/
Accept: text/*; q=1,application/*; q=0.9;

##HTTP/1.1 200 OK
##Date: Sat, 03 Jul 2021 23:56:33 GMT
##Content-Length: 10
##Content-Type: text/plain; charset=utf-8
##
##text/html:

####
GET http://localhost:8080/
Accept: text/plain; q=1,application/xml; q=1;

## HTTP/1.1 406 Not Acceptable
## Date: Sat, 03 Jul 2021 19:17:28 GMT
## Content-Length: 0
## Connection: close

Conclusion

Let’s not forget about the Accept header even if this feature is not implemented in the current framework.

If you use Golang and want to work with the Accept header or mime types in general, you could try mimeheader library. I believe it will help with the task.


Share this post on:

Previous Post
Use secure random number generators, please
Next Post
Quick review of the most popular ways to implement flags