📖 2 min read (~ 400 words).

Negotiation in plain net/http

The server-middleware module ships content negotiation as a standalone, dependency-free package. You can drop it into any net/http application — no spec, no analyzer, no go-openapi/runtime import.

Install

go get github.com/go-openapi/runtime/server-middleware

The full module pulls only the standard library at runtime (testify is _test.go-only).

Pick a response Content-Type

const mediaTypeXML = "application/xml"

// Pet is the demo resource served by the negotiation handler.
type Pet struct {
	XMLName xml.Name `json:"-"    xml:"pet"`
	Name    string   `json:"name" xml:"name"`
}

func pickContentType() {
	pet := Pet{Name: "Lassie"}
	offers := []string{"application/json", mediaTypeXML}

	http.HandleFunc("/pet", func(w http.ResponseWriter, r *http.Request) {
		chosen := negotiate.ContentType(r, offers, "application/json")
		w.Header().Set("Content-Type", chosen)

		switch chosen {
		case mediaTypeXML:
			_ = xml.NewEncoder(w).Encode(pet)
		default:
			_ = json.NewEncoder(w).Encode(pet)
		}
	})

	srv := &http.Server{
		Addr:              ":8080",
		ReadHeaderTimeout: readHeaderTimeout,
	}
	log.Fatal(srv.ListenAndServe())
}

Full source: docs/examples/contenttypes/negotiatestandalone/main.go

ContentType returns the most-acceptable offer per the request’s Accept header (q-values, specificity, position-as-tiebreaker). If no offer is acceptable, the third argument (the default offer) is returned.

Pick a Content-Encoding

chosen := negotiate.ContentEncoding(r, []string{"gzip", "deflate"})
if chosen != "" {
	w.Header().Set("Content-Encoding", chosen)
}

Full source: docs/examples/contenttypes/negotiatestandalone/main.go

"" means “no offer is acceptable” — let your handler decide whether to send the unencoded body or 406.

Exercise

# JSON by preference
curl -i -H 'Accept: application/json' http://127.0.0.1:8080/pet

# XML preferred, JSON acceptable
curl -i -H 'Accept: application/xml;q=0.9, application/json;q=0.5' \
        http://127.0.0.1:8080/pet

# Both rejected → falls back to the default offer (application/json here)
curl -i -H 'Accept: text/html' http://127.0.0.1:8080/pet

MIME-parameter behaviour

As of v0.30 the negotiator honours MIME parameters by default — an Accept of text/plain;charset=utf-8 does not match an offer of text/plain;charset=ascii. Pre-v0.30 the parameters were stripped before matching. Opt out per call to restore the old behaviour:

chosen := negotiate.ContentType(r, offers, "",
	negotiate.WithIgnoreParameters(true),
)

Full source: docs/examples/contenttypes/negotiatestandalone/main.go

Full algorithm and rationale: standalone / content negotiation.

Adding a Swagger UI to the same server

The same module ships docui — stdlib-only handlers for Swagger UI / RapiDoc / Redoc. Combining the two gives you a small spec-served, doc-UI-equipped HTTP server with no OpenAPI runtime dependency at all. See docui standalone (queued) once we write that example.