While building web applications, you may come across some cases that you want to write some functionalities that can be shared between your HTTP handlers. In that case, you will end up writing a middleware. The idea of middleware is only to create a wrapper that does the same thing before and after the main handler, where your code concentrates on the more important business logic.
Go programming language comes with a really good HTTP manipulating package. You can write good web applications solely using the native HTTP package only. But the package requires a low-level understanding to work with. It is hard and requires much verbosity to create a wrapper over the main function from time to time.
To create a proper middleware, we need to retrieve the information from the HTTP request and do something over it then pass it to the next handler. But the response write is write-only, and when you finished writing anything into that, the round trip is complete and you can do anything else. Those limits make it harder to create good middleware. Not to mention, that in most cases, you also need to chain a lot of middleware to make an HTTP handler work perfectly.
In this article, I will come through some practices that usegomw
to make the creating middleware process become easier. You can get it at:
go get -u github.com/vchitai/gomw
Authentication middleware
The authentication use case is one of the most common use cases in web applications. Normally the client should forward an authenticate token that contains an encoded claim that can be validated from the backend server. In other cases, the client may pass a meaningful token so that the backend server can look up corresponding claims in the data source. For either case, we must write a common function that allows us to check for the validity of the claim even before entering the main code logic, keeping the main logic being polluted with the checking claims case at every possible moment. The claims may be useful for the lower layer. The parsed identification should be injected again so that the below layer can access it at ease and no need to redo the retrieval process twice.
The authentication process will be triggered before entering the main handler. Our authentication middleware reads the token from the `Authorization` header. The basic authorization is supposed to be in the format of basic authentication “user:password”, encoded using base64 encoding techniques. After decoding and splitting to get the available token, we will reinject it into the context, so that in the next handler, it’s can be retrieved and do further process.
type claim struct {
UserID string `json:"user_id"`
UserPassword string `json:"user_password"`
}
type authClaimKey struct{}
func claimFromToken(token string) (*claim, error) {
var decodedToken []byte
if _, err := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(token)).Read(decodedToken); err != nil {
return nil, err
}
segments := strings.Split(string(decodedToken), ":")
if len(segments) != 2 {
return nil, fmt.Errorf("token does not have valid number of segments")
}
return &claim{
UserID: segments[0],
UserPassword: segments[1],
}, nil
}
func ExtractClaimFromToken(request *http.Request) (*http.Request, error) {
token := request.Header.Get("Authorization")
if len(token) == 0 {
return request, fmt.Errorf("require authorization token included")
}
claim, err := claimFromToken(token)
if err != nil {
return request, fmt.Errorf("extract claim error %w", err)
}
newCtx := context.WithValue(request.Context(), authClaimKey{}, claim)
request = request.WithContext(newCtx)
return request, nil
}
Request — response logging middleware
Another common case that you should write a middleware is request-response logging. Normally we don’t want it in other environments, but the development environment. That will cause a lot of meaningless flood of information in a production environment, where the metrics become better to help you know the situation. But in the development environment, the debug logs provide lots of meaningful things to help you quickly analyze the code block and debugging process.
The logging process comes up both before and after the main processing. When the request is coming, we should log what the client has requested (the resources and the payload if needed). When the response is created, we should log in to see what have the internal process done.
func PayloadLoggingHTTPMiddleware() gomw.HTTPMiddleware {
return gomw.NewHTTPMiddleware(func(writer http.ResponseWriter, request *http.Request) (*http.Request, bool) {
bodyReader, err := request.GetBody()
if err != nil {
// not logging, pass through this step
return request, true
}
var body []byte
if _, err := bodyReader.Read(body); err != nil {
// not logging, pass through this step
return request, true
}
log.Println("A request was recorded", "url", request.URL.String(), "payload", string(body))
return request, true
}, func(response gomw.HTTPResponse, request *http.Request) gomw.HTTPResponse {
log.Println("A response was recorded", "url", request.URL.String(), "payload", string(response.Body()), "code", response.Code())
return response
})
}
Error wrapping middleware
In the go language, errors are passing around like a value, and we should handle it gracefully inside our applications. But that’s may make too many duplicate code blocks sometimes. And there are many cases that we forget it and simply return the error directly from inside. That may create many insecure vulnerabilities that the attackers can use against us. So wrapping another processing layer to our code is a good way to protect our server.
We should put that layer after we have done everything in order to check the returned error if it’s is unhandled (in the form of 500 internal server errors). We will wrap over it and return a common set of error messages. Some logs or alerts may be hooked to further investigate the case, too.
func ErrorHidingHTTPMiddleware() gomw.HTTPMiddleware {
return gomw.NewHTTPAfterMiddleware(func(response gomw.HTTPResponse, request *http.Request) gomw.HTTPResponse {
if response.Code() == http.StatusInternalServerError {
return gomw.NewHTTPResponse([]byte(http.StatusText(http.StatusInternalServerError)), http.StatusInternalServerError)
}
return response
})
}
Request validation middleware
One of the most fundamental principles of the backend server is that we should never trust the client's requests. We should always validate the client's requests before handling them. There is a lot of validators available out there, you can use go’s the standard validator or the JSON schema, or something else. But we do not want to call the request to validate function everywhere in the HTTP handler. So let a middleware do that for you.
For the most generic validate function, we need a function that accepts the request from clients and returns errors if anything in the request is invalid. This middleware will take the frontline position. When received the request, we pass it into the validate function and return the error immediately if it’s failed the validation.
type ValidateFunc func(request *http.Request) error
func ValidationHTTPMiddleware(validateFunc ValidateFunc) gomw.HTTPMiddleware {
return gomw.NewHTTPBeforeMiddleware(func(writer http.ResponseWriter, request *http.Request) (*http.Request, bool) {
if err := validateFunc(request); err != nil {
log.Printf("Validation error: %v\n", err)
// terminate the request instantly
writer.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte(err.Error()))
return request, false
}
return request, true
})
}
Metrics recording middleware
In the production environment, we cannot rely on logging anymore. When the number is so large, only the extracted metrics make more sense than the detailed one. You are may familiar with some metric platforms. But in this case, I will take you over the most common one, Prometheus. In this example, I will implement common metrics that measure the HTTP handling duration.
Whenever a request is coming, we start a timer, and when it is done, we concluded that timer and create an observation of that period into the Prometheus histogram metrics. The metrics are then processed to provide further information later.
var httpLatencyHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "http.server.duration",
Buckets: prometheus.DefBuckets, // seconds
}, []string{"operation"})
type ctxKey string
type latencyMeasure struct {
startTime time.Time
}
func LatencyRecordingHTTPMiddleware(operation string) gomw.HTTPMiddleware {
return gomw.NewHTTPMiddleware(func(writer http.ResponseWriter, request *http.Request) (*http.Request, bool) {
measure := latencyMeasure{startTime: time.Now()}
newCtx := context.WithValue(request.Context(), ctxKey("latency-measure"), &measure)
request = request.WithContext(newCtx)
return request, true
}, func(response gomw.HTTPResponse, request *http.Request) gomw.HTTPResponse {
measure, ok := request.Context().Value(ctxKey("latency-measure")).(*latencyMeasure)
if !ok {
return response
}
hist, err := httpLatencyHistogram.GetMetricWithLabelValues(operation)
if err != nil {
return response
}
hist.Observe(time.Since(measure.startTime).Seconds())
return response
})
}
Tracing span initialize middleware
When the application achieved a stable state, you may switch your concentration to performance optimization. In that case, collecting the tracing information for further investigation is a must. Recently, the OpenTelemetry platform, replacing old OpenTracing, is a reasonable choice, so let’s choose it to implement this example. You can always switch to another one if possible. All the implementation can be switched at any time if you put it on a well-designed interface.
To set up the tracing span, you need to create a trace span and inject it into the request context. This process should be in front of all the layers. The tracing span can later be extended in the next layer, to collect more events for the span. After all the processes have been done, we will put an end to that span.
var instrumentationName = "net/http"
type ctxKey string
func TraceStartHTTPMiddleware() gomw.HTTPMiddleware {
return gomw.NewHTTPMiddleware(func(writer http.ResponseWriter, request *http.Request) (*http.Request, bool) {
ctx, span := otel.GetTracerProvider().Tracer(instrumentationName).Start(request.Context(), "server request")
ctx = context.WithValue(ctx, ctxKey("span-end"), span)
request = request.WithContext(ctx)
return request, true
}, func(response gomw.HTTPResponse, request *http.Request) gomw.HTTPResponse {
span, ok := request.Context().Value(ctxKey("span-end")).(trace.Span)
if !ok {
return response
}
span.End()
return response
})
}
Conclusion
In this article, I have shown you how to implement some common HTTP middleware. The idea is only to create a wrapper that does the same thing before and after the main handler, where your code concentrates on the more important business logic. Those presented in the article show the idea and most common mechanism those functions may look like. In most real-life cases, these may be the sole things you need to create a good go HTTP application.