Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,27 @@ jobs:
*) echo "expected the inbox or a redirect to /login"; docker logs og; exit 1 ;;
esac

- name: The landing page is public
- name: The landing page is public and actually has a body
run: |
# / must render for a signed-out visitor, not redirect to /login.
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)
echo "GET / -> $code"
# --compressed matters: plain curl sends no Accept-Encoding, but every
# browser does. A proxy that forwards the child's content-encoding
# while fetch has already decompressed the body returns a 200 with
# zero bytes — which shipped once, and which a bare curl cannot see.
code=$(curl -s --compressed -o /tmp/landing.html -w '%{http_code}' http://127.0.0.1:8080/)
bytes=$(wc -c < /tmp/landing.html)
echo "GET / -> $code, $bytes bytes"

[ "$code" = "200" ] || { echo "the landing page must be public"; docker logs og; exit 1; }
[ "$bytes" -gt 1000 ] || { echo "landing page body is empty or truncated"; docker logs og; exit 1; }

curl -s http://127.0.0.1:8080/ | grep -q "warm conversations" \
grep -q "warm conversations" /tmp/landing.html \
|| { echo "landing page did not render its content"; exit 1; }

# The signed-in shell must survive compression too.
login_bytes=$(curl -s --compressed http://127.0.0.1:8080/login | wc -c)
echo "GET /login -> $login_bytes bytes"
[ "$login_bytes" -gt 500 ] || { echo "/login body is empty"; docker logs og; exit 1; }

- name: Auth is enforced
run: |
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/v1/recommendations)
Expand Down
28 changes: 26 additions & 2 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,19 @@ async function proxyToWeb(request: Request): Promise<Response> {
const incoming = new URL(request.url);
const target = new URL(incoming.pathname + incoming.search, `http://127.0.0.1:${WEB_PORT}`);

// Ask the child for identity encoding. `fetch` transparently decompresses
// what it receives, so a forwarded `content-encoding: gzip` would describe a
// body that is no longer compressed — the client then fails to decode it and
// renders nothing. Not requesting compression over loopback avoids the
// mismatch entirely and costs nothing.
const headers = new Headers(request.headers);
headers.set('accept-encoding', 'identity');

let response: Response;
try {
return await fetch(target, {
response = await fetch(target, {
method: request.method,
headers: request.headers,
headers,
body: request.method === 'GET' || request.method === 'HEAD' ? undefined : request.body,
redirect: 'manual',
// Streaming bodies require this; without it Bun buffers and rejects.
Expand All @@ -126,6 +135,21 @@ async function proxyToWeb(request: Request): Promise<Response> {
console.error('web proxy failed', error);
return new Response('the web application is not responding', { status: 502 });
}

// Strip hop-by-hop and body-framing headers. Whatever the child said about
// length or encoding describes its own wire format, not the one this
// response will be sent with.
const outbound = new Headers(response.headers);
outbound.delete('content-encoding');
outbound.delete('content-length');
outbound.delete('transfer-encoding');
outbound.delete('connection');

return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: outbound,
});
}

// ------------------------------------------------------------------- server
Expand Down
Loading