Skip to content

Instantly share code, notes, and snippets.

@gabesullice
Last active February 5, 2021 19:34
Show Gist options
  • Save gabesullice/435f9a1597ace900bc62aa2ce66043d0 to your computer and use it in GitHub Desktop.
Save gabesullice/435f9a1597ace900bc62aa2ce66043d0 to your computer and use it in GitHub Desktop.
Tiny Go server to test browser implementations of the vary: accept header
"use strict"
// Fetches the browser window's URL with an accept header set to the mediaType.
async function doFetch(mediaType, host) {
const loc = window.location
const url = `${loc.protocol}//${host || loc.host}${loc.pathname}`;
const options = {
headers: {
accept: mediaType,
},
}
const response = await fetch(url, options)
return mediaType.startsWith('text/html')
? response.text().then(parseHTML).then(getAcceptHeaderFromHTMLDoc)
: response.json()
}
// Given the expected DOM document node, get the "echoed" Accept header. Every
// browser sends a different value, which is why it's not hardcoded.
function getAcceptHeaderFromHTMLDoc(doc) {
const accept = doc.querySelector('body code').innerText
return { accept }
}
// Given a string of HTML, parse it into a DOM document node.
function parseHTML(html) {
const dom = new DOMParser();
const doc = dom.parseFromString(html, 'text/html')
return doc
}
// Serially makes requests and logs the responses received If the logging
// breaks or the log messages do not match expectaions, it means some HTTP
// cache is not respecting the vary header and a cached response for a
// different accept header value was incorrectly reused.
document.addEventListener('DOMContentLoaded', async function doFetches() {
let last = null
// Should be cached because this replicates the browser's first page load.
last = await doFetch(getAcceptHeaderFromHTMLDoc(document).accept)
console.log(last)
// Should not be cached since it's a new request to a new host.
last = await doFetch("application/json", "127.0.0.1:8880")
console.log(last)
// Should be cached no matter what.
last = await doFetch("application/json", "127.0.0.1:8880")
console.log(last)
// Should not be cached since it's a new accept header.
last = await doFetch("application/hal+json,application/json;q=0.9")
console.log(last)
// Should be cached no matter what.
last = await doFetch("application/hal+json,application/json;q=0.9")
console.log(last)
// Ideally, this would be cached, but it won't be since browsers only cache
// one response per URL at a time. It will have been "overridden" by the
// hal+json response.
last = await doFetch("application/json")
console.log(last)
// Should be cached no matter what.
last = await doFetch("application/json")
console.log(last)
// Ideally, this would be cached, but it won't be since browsers only cache
// one response per URL at a time. It will have been "overridden" by the last
// json response.
last = await doFetch(getAcceptHeaderFromHTMLDoc(document).accept)
console.log(last)
})
package main
import "fmt"
import "net/http"
import "strings"
var jsFile = "/fetcher.js"
func main() {
h := http.NewServeMux()
// Echo the accept header without adding a `vary: accept` response header.
// The response should be broken. Either the HTML page will show
// "application/json" (not the right accept header) or the JavaScript console
// will fail to parse JSON (since it's receiving HTML).
h.Handle("/echo-accept-header", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("cache-control", "public, max-age=120, immutable")
echoAcceptHeader(w, r)
}))
// Echo the accept header and add a `vary: accept` response header.
// The response should not b broken. The HTML page will show an accept header
// value that begins with "text/html" (each browser is slightly different)
// and the JavaScript console should echo an object that contains the accept
// header value "application/json".
h.Handle("/echo-accept-header-w-vary", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("vary", "accept")
w.Header().Add("cache-control", "public, max-age=120, immutable")
w.Header().Add("access-control-allow-origin", r.Header.Get("origin"))
echoAcceptHeader(w, r)
}))
// Serve the test JS program.
h.Handle(jsFile, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("cache-control", "no-cache")
w.Header().Add("content-type", "application/javascript")
http.ServeFile(w, r, "."+jsFile)
}))
http.ListenAndServe(":8880", h)
}
// Inspects the request's accept header. Responds with either HTML, JSON, or a
// 406 Not Acceptable for an unrecognized value.
func echoAcceptHeader(w http.ResponseWriter, r *http.Request) {
accept := r.Header.Get("accept")
types := strings.Split(accept, ",")
switch types[0] {
case "text/html":
echoAcceptHeaderHTML(w, r)
case "application/json":
echoAcceptHeaderJSON(w, r)
case "application/hal+json":
echoAcceptHeaderJSON(w, r)
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}
// Serves an HTML response with an inline JavaScript program which fetches the
// same URL that served the request and specificies an
// `accept: application/json` header value, the it parses the response and logs
// it. An error will appear in the console or the browser window will show a
// JSON object if the browser is caching the response without varying by the
// accept header.
func echoAcceptHeaderHTML(w http.ResponseWriter, r *http.Request) {
accept := r.Header.Get("accept")
w.Header().Set("content-type", "text/html")
html := fmt.Sprintf("<html><link rel=\"icon\" href=\"data:,\"><script src=\"%s\"></script><body><code>%s</code></body></html>", jsFile, accept)
fmt.Fprintf(w, html)
}
// Serves a JSON response containing a JSON object containing the value of the
// request's accept header.
func echoAcceptHeaderJSON(w http.ResponseWriter, r *http.Request) {
accept := r.Header.Get("accept")
w.Header().Set("content-type", "application/json")
fmt.Fprintf(w, "{\"accept\": \""+accept+"\"}")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment