Last active
February 5, 2021 19:34
-
-
Save gabesullice/435f9a1597ace900bc62aa2ce66043d0 to your computer and use it in GitHub Desktop.
Tiny Go server to test browser implementations of the vary: accept header
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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