fix(edge): serve an Openship page for upstream-down 502/504 instead of OpenResty's stock one - #557
Open
AbdullahM07 wants to merge 1 commit into
Open
Conversation
The edge defined no `error_page` for any 5xx, so an app that was restarting, crashed, or still booting produced OpenResty's stock `502 Bad Gateway`. Behind Openship Cloud's shared edge — which forwards to the box by IP and relays whatever the box answers — that page is what a visitor to the operator's OWN domain reads, branded for a third party. Closes oblien#556. Adds the page as `edge-upstream-down.ts`, mirroring the unrouted-host page (oblien#431): inline in the config for the same reason (it has to arrive on every install path at once, and a missing `include` would be a hard `-t` failure that freezes every later reload), the same five tokenizer invariants, and no links or reflected values since it is served to strangers. Removing the third-party link IS the fix — re-pointing it at the operator's own domain would only link to the site that is currently down. Wired at server scope in every vhost that proxies to an upstream: not host redirects, and not static routes unless a compiled vercel.json gave them proxy locations. Intercepts 502 and 504 only — `blockStatus` (403) and `rateLimit.status` (429) are operator-overridable, so a 503 arm would brand a deliberate block as an outage. `VHOST_GENERATION` 1 -> 2, without which this reaches no existing box: `registerRoute` is the sole writer, and `reapplyStoredRoutes` replays only vhosts stamped below the current generation. No `=` before the named location, so an intercepted 504 stays a 504 and one body serves both codes. Verified against openresty/openresty:1.27.1.1-alpine rather than assumed: with `=`, the named location's own return code replaces the original and a timeout stops being reportable as one. Verified: a new e2e boots real OpenResty with vhosts from the real NginxProvider and checks a refused upstream (502 + our page), a read timeout (504 + our page), an app's own 502 (its body passes through untouched, since `proxy_intercept_errors` is off), a healthy app, a `.css` URI on a down app, and a static vhost. Three of its six cases fail without this change, returning the stock pages. Full adapters suite: 2879 pass.
AbdullahM07
force-pushed
the
fix/edge-upstream-down-page
branch
from
August 16, 2026 11:51
4743cdd to
394c06b
Compare
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #556.
What was broken
git grep error_pageover the repo returned zero hits: the edge defined no page for any5xx, so an app that was restarting, crashed, or still booting produced OpenResty's stock
502 Bad Gateway. Behind Openship Cloud's shared edge — which forwards to the box by IPand relays whatever the box answers (
edge-target.ts) — that stock page is what a visitorto the operator's own domain reads, carrying a third party's branding and links.
What this does
Serves an Openship-branded page for upstream-down errors from the box's own edge, mirroring
what #431 did for unrouted hosts.
packages/adapters/src/infra/edge-upstream-down.ts— the page, built exactly likeedge-not-found.ts: inline in the config (it has to arrive on every install path at once,and a shared
includewould be a hard-tfailure that freezes every later reload on thebox), the same five tokenizer invariants, and no links or reflected values since it is
served to strangers. Removing the third-party link is the fix — re-pointing it at the
operator's own domain would only link to the site that is currently down.
and not static routes unless a compiled
vercel.jsongave them proxy locations — whichis why the gate tests for an upstream rather than for
!staticRoot.blockStatus(default 403) andrateLimit.status(default 429) are operator-overridable and
limit_req_statusis 429, so a 503 arm wouldbrand an operator's deliberate block as an outage.
VHOST_GENERATION1 → 2. Without the bump this reaches no existing box:registerRouteis the sole writer andreapplyStoredRoutesreplays only vhosts stampedbelow the current generation.
=before the named location, which is load-bearing rather than stylistic — seebelow.
nginx semantics, verified rather than assumed
error_page … @namedpreserves or clobbers the status code depending on=, so I probed itin
openresty/openresty:1.27.1.1-alpinewith dead and hanging upstreams before settling thedesign:
error_page 502 504 @loc;error_page 502 504 = @loc;==So the form without
=lets one shared ~2 KB body cover both codes without collapsing 504into 502. Case D is the one most worth having: with
proxy_intercept_errorsoff (nginx'sdefault, and set nowhere in this repo) an app that answers 502 itself keeps its own body, so
this page never hijacks a real response from a running app. That default is inherited rather
than written in any vhost, so the e2e below is what fails if a future
proxy_intercept_errors on;ever starts masking every app's real error responses.How it was verified
apps/api/test/e2e/edge-upstream-down-page.e2e.test.ts— boots real OpenResty withvhosts from the real
NginxProviderand checks: a refused upstream (502 + our page,and explicitly not the stock body), a read timeout (504 + our page), an app's own 502
(its body passes through untouched), a healthy app (200, untouched), a
.cssURI on adown app (
text/html, per the emptytypes { }map), and a static vhost (never servesit).
openresty -truns inside the same container first, making this the pre-mergeequivalent of the Dockerfile's gate.
<title>502 Bad Gateway</title>and<title>504 Gateway Time-out</title>— the reportedbug itself. Confirmed by temporarily disabling the wiring and re-running. The other three
pass either way by design: they guard against regressions rather than proving the feature.
edge-upstream-down.test.ts(16) — the tokenizer invariants plus thestripComments+extractBlocksround trip that Unrecognized hostnames return raw TLS / OpenResty errors instead of a friendly “Service Not Found” page #431's fix originally broke.nginx.test.ts(+7) — handler in both blocks of a proxy vhost, absent from static andhost-redirect vhosts, present on a static vhost that has proxy locations,
error_pageatserver scope, no 503, no
=.@repo/adapterssuite: 2879 pass.tsc --noEmitclean in@repo/adaptersand@repo/api.bun formatwas deliberately not run across the repo:nginx.ts,nginx.test.tsandindex.tsalready carry Prettier drift onmain, and--writewould have reformatted linesthis change never touches. The two new files pass
prettier --checkas written.Known limitation
This covers every 502/504 the box generates — an app that is down, on a custom domain or
a free
*.opsh.ioone. It does not cover the case where the box itself is unreachablefrom Openship Cloud's edge: that response is generated entirely by the Cloud edge and no
change in this repository can reach it. #556 records the same boundary.
The page carries an
openship-edge-upstream-downmarker immediately after the doctype, socurl -s https://host | head -c 200tells the two cases apart in support.