---
title: 'HTTP Server'
source: 'https://academia.sh/en/courses/go-applications/http-server'
course: 'The Standard Library and Network Applications'
language: en
updated: '2026-09-14T10:59:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures by running it with `net/http/httptest` that none of an HTTP request's path, header, and body carry type information"
    - 'Builds a middleware chain wrapping a handler by hand and shows that every ring in the chain is written by the calling side'
    - 'Proves by running it that the content type header is a hint and that a wrong hint produces no error on the server side'
---

# HTTP Server

The course's opening lesson. That none of a request's path, header, and body carry type information is measured with `net/http/httptest`, without opening a real port; the middleware pattern runs as a three-ring chain in which one handler wraps another. The limiting measure: the content type header is a hint, not a type — a wrong hint produces no error on the server side, the error only surfaces on the side that tries to resolve the body against that hint.

The previous course's closing lesson measured how a goroutine stops and showed that
stopping mostly depends on **work done by someone else**: without a channel closing,
without a context being canceled, a waiting goroutine could not make its own decision. That
measurement always stayed **inside** the program — the two goroutines were part of the same
compiled binary, bound to the same type system. This lesson carries the question **outside**
the program: what happens when a value goes to the network, a file, a template, or a
database? The other side did not pass through the same compiler, did not see the same type
definitions — sometimes it was not even written in the same language. The whole course asks
this one question at twelve different boundaries: **what happens to a value's type
information as it crosses a boundary?** This lesson starts with the boundary crossed most
often: an HTTP request.

What a server does, who handles which request when, and how middleware is ordered in a
chain were already built as concepts in earlier courses. None of that is repeated here; this
lesson asks a different question — not the server's **task**, but the relationship between
the data **entering and leaving the server** and the type system. This distinction holds for
the rest of the course: every lesson measures not a layer's task but the fate of the data at
that layer's boundary.

## What the Handler Receives: Path, Header, Body

When a client sends an HTTP request, the only source for what is inside that request is the
byte sequence that crosses the wire. Go's standard library parses this byte sequence and
turns it into an `*http.Request` value, but what gets turned into it is still three raw
pieces: the **path** (`r.URL.Path`, a string), the **headers** (`r.Header`, a map from
string to string slice), and the **body** (`r.Body`, a reader that streams bytes). In the
sending side's mind the path might be an integer ID, the header a structured value, the
body a record — but after crossing the boundary, what is left is only strings and bytes.
Go's type system says "this was really an integer" or "this was really a struct" about none
of these three; it has no authority to say so, because it does not know which program is
running on the other end of the wire or which types that program defines.

The source below builds this measurement's shared parts: a helper that classifies and
prints results, a core handler that reads the request's path and one header and writes the
body's length as the response, and two middleware layers that wrap this core.

```go
// requestcore.go — the shared parts of the request handler and middleware
package main

import (
	"fmt"
	"io"
	"net/http"
)

var partCount, lostCount int
var lastReadBody []byte

// measureLine prints one of this lesson's classes (preserved / rebuilt / lost)
// alongside a piece of evidence and keeps a running total.
func measureLine(part, class, evidence string) {
	partCount++
	if class == "lost" {
		lostCount++
	}
	fmt.Printf("%-30s %-10s %s\n", part, class, evidence)
}

// coreHandler reads the incoming request's path, one header, and body, and
// writes a plain-text response. It adds a fixed content type hint to the
// response; checking whether that hint is correct is not this function's job.
func coreHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	lastReadBody = body
	id := r.Header.Get("X-Request-ID")
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "path=%s id=%s body-length=%d", r.URL.Path, id, len(body))
}

// loggingMiddleware increments a counter before calling the handler it wraps;
// it does not touch the body or the connection.
func loggingMiddleware(next http.Handler, counter *int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*counter++
		next.ServeHTTP(w, r)
	})
}

// headerMiddleware adds a fixed header to the response and continues the chain.
func headerMiddleware(next http.Handler, counter *int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*counter++
		w.Header().Set("X-Chain-Step", "passed")
		next.ServeHTTP(w, r)
	})
}
```

The measurement runs this core with one request. The request is built with
`net/http/httptest` — `httptest.NewRequest` produces an `*http.Request` in memory without
opening a real port, and `httptest.NewRecorder` gives a fake `http.ResponseWriter` that
writes the response to a buffer instead of a real connection. There is no real socket,
address, or duration between client and server.

```go
// requestmeasurement.go — the type information of a request's parts and the middleware chain
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- what the handler receives: bytes and text --")

	body := bytes.NewBufferString("qty=3")
	request := httptest.NewRequest(http.MethodPost, "/products/42", body)
	request.Header.Set("X-Request-ID", "1001")

	var logCount, headerCount int
	chain := loggingMiddleware(
		headerMiddleware(http.HandlerFunc(coreHandler), &headerCount),
		&logCount,
	)

	recorder := httptest.NewRecorder()
	chain.ServeHTTP(recorder, request)

	measureLine("request path (r.URL.Path)", "lost",
		fmt.Sprintf("value=%q type=%T", request.URL.Path, request.URL.Path))
	measureLine("request header (Header.Get)", "lost",
		fmt.Sprintf("value=%q type=%T", request.Header.Get("X-Request-ID"), request.Header.Get("X-Request-ID")))
	measureLine("request body (bytes read)", "lost",
		fmt.Sprintf("type=%T length=%d", lastReadBody, len(lastReadBody)))

	fmt.Println("-- middleware chain --")
	fmt.Println("chain: loggingMiddleware( headerMiddleware( coreHandler ) ) - 3 rings")
	fmt.Printf("logging calls=%d header calls=%d (both written by this lesson)\n",
		logCount, headerCount)
	fmt.Printf("status code=%d chain header=%s\n", recorder.Code, recorder.Header().Get("X-Chain-Step"))
	fmt.Println("response body:", recorder.Body.String())

	fmt.Printf("of this request's %d parts, carrying type information=%d (lost=%d)\n",
		partCount, partCount-lostCount, lostCount)
}
```

```
-- what the handler receives: bytes and text --
request path (r.URL.Path)      lost       value="/products/42" type=string
request header (Header.Get)    lost       value="1001" type=string
request body (bytes read)      lost       type=[]uint8 length=5
-- middleware chain --
chain: loggingMiddleware( headerMiddleware( coreHandler ) ) - 3 rings
logging calls=1 header calls=1 (both written by this lesson)
status code=200 chain header=passed
response body: path=/products/42 id=1001 body-length=5
of this request's 3 parts, carrying type information=0 (lost=3)
```

**ND1.** The path being `"/products/42"` suggests that the digit `42` should **mean** an
integer — in the source code this value was a product ID. But when the measurement asks with
`%T`, the answer is uniform: `string`. Go's router does not automatically convert any digit
sequence to a number while parsing the path; `/products/42` and `/products/forty-two` are two
strings of the same type, and the only difference between them is their content. The same
observation holds for the header: the value `"1001"` in the request ID header is also a
string, even if it is a numeric ID in the sending side's mind. Nothing that crosses the wire
carries its own type; the only thing it carries is characters.

**ND2.** The body is the clearest of these three: `io.ReadAll(r.Body)` always returns
`[]byte`, regardless of whether the body's content is a number, a struct, or random text. The
measurement shows `length=5` because the string `"qty=3"` is five bytes — but this length
does not say **what** the body represents, only **how many bytes** it is. All three of these
lines fall into the `lost` class; of the request's three parts measured by this lesson, zero
carry type information. The full distribution of the course's thirteen boundary crossings (1
preserved, 5 rebuilt, 7 lost) is built in the JSON Serialization lesson; this lesson only
prints its own three-line slice.

## Middleware: One Handler Wrapping Another

`http.Handler` is an interface defined by a single method in Go's standard library:
`ServeHTTP(http.ResponseWriter, *http.Request)`. This smallness is a deliberate design
decision — a handler can be **anything** that implements this single method, so embedding
one handler inside another needs no special framework, an ordinary function call is enough.
Both `loggingMiddleware` and `headerMiddleware` follow the same pattern: each takes an
`http.Handler`, returns a new `http.HandlerFunc` that runs its own logic, and inside the
body of the handler it returns, calls the `ServeHTTP` method of the handler it received.
The result is a **chain**: the outer ring runs first, does its work, and calls the inner
ring, which does its own work and calls the next one — until the core handler, at the
innermost point, stops the chain.

In the source code this chain is built by hand with a single statement in the `main`
function, by placing the `headerMiddleware` call inside the `loggingMiddleware` call, and
the core handler inside that: three rings, all three written by this lesson, none discovered
automatically from a registry or a tag. The output lines `logging calls=1` and
`header calls=1` count this directly: both middleware layers in the chain were called
exactly once, because the request also arrived exactly once. What a server's task is, the
general structure of a request-processing chain, and why middleware exists were already
built as concepts in earlier courses; what is measured here is that concept's concrete
counterpart in Go's standard library — there is no hidden mechanism beyond one handler
wrapping another.

**ND3.** The chain's order affects the outcome. `headerMiddleware` sets the `X-Chain-Step`
header inside its own `ServeHTTP` body, **before** calling the inner handler; but this
header is only locked into the response the moment `w.WriteHeader` is called, and it is
`coreHandler` that calls `WriteHeader`. So the header `headerMiddleware` adds succeeds in
reaching the response, because it is set before the core writes the response — the output
line `chain header=passed` confirms this. If the order were reversed, that is, if
`headerMiddleware` ran **after** the core, the header would be silently dropped because
`WriteHeader` would already have been called — Go does not error in this case, it just
ignores the header — unless the status code is in the 1xx class and the header is marked as
a trailer.

**ND4.** Both middleware layers share the same signature: each takes an `http.Handler` and a
counter pointer and returns a new `http.Handler`. This uniformity reduces adding a third
middleware to the same chain to a one-line wrap — adding a new ring to the chain is not a
different operation from adding a new element to a list, it is just placing one function
call inside the previous one. But this convenience has a cost: the longer the chain, the
more nested function calls must be read to see how many rings a request passes through
before reaching `coreHandler`. The chain itself does not stand anywhere as a list — it is
only a single statement in the `main` function.

This also determines the chain's relationship to **type information**: neither
`loggingMiddleware` nor `headerMiddleware` looks at the content of the request or the
response to do its job; both only pass the `*http.Request` and `http.ResponseWriter`
values on to the next ring as they are. If a middleware layer wanted to read a field in the
request's body, it would run into the same boundary seen throughout this lesson — the body
would still be only bytes, and the middleware could only assign meaning to those bytes with
parsing code it wrote itself.

## The Limiting Measure: Content Type Is a Hint, Not a Type

The `Content-Type: application/json` header the core handler adds to its response looks, at
first glance, like a guarantee of the response body's type. It is not. This header is
nothing more than a string, and no mechanism steps in to check whether the body is really in
that format, no matter who reads it. The measurement below deliberately breaks this: the
handler gives an `application/json` hint but writes plain text to the body. This is a
different observation from the three `lost` lines in the previous measurement: those lines
measured that the information the request carried **never existed at all**, while this
measurement shows that a piece of information that does exist (the header itself) **can be
wrong** and that nothing catches this anywhere.

```go
// wronghint.go — the content type is a hint; its error surfaces not while running but while resolving
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

// fakeHandler writes plain text to the body and gives an "application/json"
// hint in the header. No code checks the agreement between the hint and the body.
func fakeHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "this body is not json, it is plain text")
}

func main() {
	fmt.Println("-- content type is a hint, not a type --")

	request := httptest.NewRequest(http.MethodGet, "/report", nil)
	recorder := httptest.NewRecorder()
	http.HandlerFunc(fakeHandler).ServeHTTP(recorder, request)

	fmt.Printf("status code=%d (succeeds even when the hint is wrong)\n", recorder.Code)
	fmt.Printf("hint in the header=%s\n", recorder.Header().Get("Content-Type"))

	var target map[string]any
	decodeErr := json.Unmarshal(recorder.Body.Bytes(), &target)
	fmt.Printf("is there an error decoding the body as json=%t\n", decodeErr != nil)
	fmt.Println("the error is not on the server, it surfaces on the side trying to resolve the body against the hint")
}
```

```
-- content type is a hint, not a type --
status code=200 (succeeds even when the hint is wrong)
hint in the header=application/json
is there an error decoding the body as json=true
the error is not on the server, it surfaces on the side trying to resolve the body against the hint
```

**ND5.** The status code is `200` — that is, the handler completed successfully without
ever noticing that the body disagreed with the hint. Nothing in the `http.ResponseWriter`
interface compares the `Content-Type` header against the body, or checks whether the body is
really valid JSON — the header is a suggestion telling the receiver "try interpreting this
body this way," not a rule the receiver must obey. The error only surfaces once one side
trusts this suggestion and calls `json.Unmarshal`; in the measurement this call happens in
the `main` function itself, and the result `decodeErr != nil` comes out `true`. There is no
warning, no log line, no error return on the server side — the boundary crossing itself is
entirely silent.

**ND6.** This observation sharpens the lesson's opening claim: the **only** hint a response
carries alongside it as it crosses the boundary is a header like this, and this hint is not
a type, it is a string — just like `r.URL.Path` or `r.Header.Get`, which also came out as
`string` when asked with `%T`. Whether the content type header is correct is left to the
discipline of the side writing the server; neither Go's compiler nor its runtime enforces
this discipline. This limiting measure shows that the information the body itself carries
and the information written in the header are two **independent** things, and that neither
confirms the other.

The standard library itself adds no check that ties the two together; the side writing the
handler is the one that would add it. If it wanted to, `coreHandler` could validate in its
own code, before the `w.Write` call, that the body is really valid JSON — but this
validation is not part of the language, it is a decision made by the writer. The only
guarantee that holds on this side of the boundary is that **nobody automatically ensures**
the consistency between header and body; every layer that establishes this consistency has
to do so explicitly in its own code.

## Summary

- An HTTP request's path, headers, and body are represented in Go's standard library as a
  string, a string, and a byte slice respectively; all three turn out to carry no special
  type when queried with `%T`.
- The measurement is built with `net/http/httptest` without opening a real port:
  `httptest.NewRequest` produces a request in memory, `httptest.NewRecorder` supplies a fake
  response writer that writes the response to a buffer.
- The middleware pattern, thanks to the `http.Handler` interface having a single method,
  amounts to nothing more than wrapping one handler inside another; every ring in the chain
  is an ordinary function call written by the caller, with no automatic discovery or
  registry.
- The order in which middleware runs affects the outcome: a header only reaches the response
  if it is added before `WriteHeader` is called; if the order is reversed, the header is
  dropped without an error.
- The content type header is a hint, not a guarantee: a wrong hint produces no error on the
  server side, the error only surfaces on the side that tries to resolve the body by
  trusting that hint.
- The full distribution of the course's thirteen boundary crossings is not built in this
  lesson; this lesson only measures the request's three parts on the server side, the JSON
  Serialization lesson builds the full table.

## Next Step

This lesson measured the boundary from the **receiving** side: what a request brings to the
server, and how blindly the server trusts the body. The next lesson asks the same boundary
from the **sending** side: what a client has to guarantee when sending a request, and what
responsibilities it takes on after receiving a response. A response body is a byte stream,
just like this lesson's request body, but this time what is measured is who closes the
stream and when, and what a timeout is built from instead of a duration.
