Skip to content

Commit cd74195

Browse files
Merge pull request web-fragments#10 from dario-piotrowicz/on-fragment-failed-fetch
feat(web-fragments): add fragment fetch failing handling
2 parents daa802a + ca257e2 commit cd74195

File tree

5 files changed

+83
-17
lines changed

5 files changed

+83
-17
lines changed
+26-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { FragmentGateway, getPagesMiddleware } from 'web-fragments/gateway';
22

3-
const gateway = new FragmentGateway({
3+
const getGatewayMiddleware: ((devMode: boolean) => PagesFunction) & {
4+
_gatewayMiddleware?: PagesFunction;
5+
} = (devMode) => {
6+
if (getGatewayMiddleware._gatewayMiddleware) {
7+
return getGatewayMiddleware._gatewayMiddleware;
8+
}
9+
10+
const gateway = new FragmentGateway({
411
prePiercingStyles: `<style id="fragment-piercing-styles" type="text/css">
512
fragment-host[data-piercing="true"] {
613
position: absolute;
@@ -13,16 +20,29 @@ const gateway = new FragmentGateway({
1320
}
1421
}
1522
</style>`
16-
});
23+
});
1724

18-
gateway.registerFragment({
25+
gateway.registerFragment({
1926
fragmentId: "remix",
2027
prePiercingClassNames: ['remix'],
2128
routePatterns: ["/", "/_fragment/remix/:_*"],
2229
// Note: make sure to run the pierced-react-remix-fragment (with remix-serve)
2330
upstream: "http://localhost:3000",
24-
});
31+
onSsrFetchError: () => {
32+
return new Response(
33+
"<p id='remix-fragment-not-found'><style>#remix-fragment-not-found { color: red; font-size: 2rem; }</style>Remix fragment not found</p>",
34+
{ headers: [["content-type", "text/html"]] }
35+
);
36+
},
37+
});
2538

26-
const gatewayMiddleware = getPagesMiddleware(gateway);
39+
getGatewayMiddleware._gatewayMiddleware = getPagesMiddleware(gateway, devMode ? 'development' : 'production');
40+
return getGatewayMiddleware._gatewayMiddleware;
41+
};
2742

28-
export const onRequest = gatewayMiddleware;
43+
export const onRequest: PagesFunction<{ DEV_MODE?: boolean }> = async (
44+
context
45+
) => {
46+
const gatewayMiddleware = getGatewayMiddleware(!!context.env.DEV_MODE);
47+
return gatewayMiddleware(context);
48+
};

e2e/pierced-react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "pnpm run --stream --parallel '/^dev:.*/'",
8-
"dev:pages": "wrangler pages dev",
8+
"dev:pages": "wrangler pages dev --binding DEV_MODE=true",
99
"dev:vite": "vite build -w",
1010
"build": "tsc -b && vite build",
1111
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",

e2e/pierced-react/tsconfig.pages-functions.json

+2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"compilerOptions": {
33
"composite": true,
44
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5+
"lib": ["ESNext"],
56
"skipLibCheck": true,
7+
"target": "ESNext",
68
"module": "ESNext",
79
"moduleResolution": "bundler",
810
"allowSyntheticDefaultImports": true,

packages/web-fragments/src/gateway/fragment-gateway.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,29 @@ export interface FragmentConfig {
2727
* This will be fetched on any request paths matching the specified `routePatterns`
2828
*/
2929
upstream: string;
30+
/**
31+
* Handler/Fallback to apply when the fetch for a fragment ssr code fails.
32+
* It allows the gateway to serve the provided fallback response instead of an error response straight
33+
* from the server.
34+
*
35+
* @param req the request sent to the fragment
36+
* @param failedRes the failed response (with a 4xx or 5xx status) or the thrown error
37+
* @returns the response to use for the document's ssr
38+
*/
39+
onSsrFetchError?: (req: RequestInfo, failedResOrError: Response|unknown) => Response|Promise<Response>;
40+
}
41+
42+
type FragmentGatewayConfig = {
43+
prePiercingStyles?: string;
3044
}
3145

3246
export class FragmentGateway {
3347
private fragmentConfigs: Map<string, FragmentConfig> = new Map();
3448
private routeMap: Map<MatchFunction, FragmentConfig> = new Map();
3549
#prePiercingStyles: string;
3650

37-
constructor(options?: { prePiercingStyles?: string }) {
38-
this.#prePiercingStyles = options?.prePiercingStyles ?? '';
51+
constructor(config?: FragmentGatewayConfig) {
52+
this.#prePiercingStyles = config?.prePiercingStyles ?? '';
3953
}
4054

4155
get prePiercingStyles() {

packages/web-fragments/src/gateway/pagesMiddleware.ts

+38-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const fragmentHostInitialization = ({ content, classNames }: {content: string; c
77

88
export function getPagesMiddleware(
99
gateway: FragmentGateway,
10+
mode: "production" | "development" = "development"
1011
): PagesFunction<unknown> {
1112
return async ({ request, next }) => {
1213
/**
@@ -19,10 +20,10 @@ export function getPagesMiddleware(
1920
* reload) in the reframed context has the correct url.
2021
*/
2122
if (request.headers.get('sec-fetch-dest') === 'iframe') {
22-
const matchedFragment = gateway.matchRequestToFragment(request);
23-
if (matchedFragment) {
24-
return new Response('<!doctype html><title>');
25-
}
23+
const matchedFragment = gateway.matchRequestToFragment(request);
24+
if (matchedFragment) {
25+
return new Response('<!doctype html><title>');
26+
}
2627
}
2728

2829
// If this is a document request, we should bail and
@@ -52,8 +53,8 @@ export function getPagesMiddleware(
5253
const requestUrl = new URL(request.url);
5354

5455
const upstreamUrl = new URL(
55-
`${requestUrl.pathname}${requestUrl.search}`,
56-
matchedFragment.upstream,
56+
`${requestUrl.pathname}${requestUrl.search}`,
57+
matchedFragment.upstream,
5758
);
5859

5960
// TODO: this logic should not be here but in the reframed package (and imported and used here)
@@ -63,12 +64,41 @@ export function getPagesMiddleware(
6364
element(element) {
6465
const scriptType = element.getAttribute('type');
6566
if(scriptType) {
66-
element.setAttribute('data-script-type', scriptType);
67+
element.setAttribute('data-script-type', scriptType);
6768
}
6869
element.setAttribute('type', 'inert');
6970
}
7071
});
71-
const fragmentRes = scriptRewriter.transform(await fetch(upstreamUrl, { ...request }));
72+
73+
const fragmentReq: RequestInfo = { ...request, url: upstreamUrl };
74+
let fragmentRes: Response;
75+
let fragmentFailedResOrError: Response | unknown | null = null;
76+
try {
77+
const response = await fetch(fragmentReq);
78+
if (response.status >= 400 && response.status <= 599) {
79+
fragmentFailedResOrError = response;
80+
} else {
81+
fragmentRes = scriptRewriter.transform(response);
82+
}
83+
} catch (e) {
84+
fragmentFailedResOrError = e;
85+
}
86+
87+
if (fragmentFailedResOrError) {
88+
if (matchedFragment.onSsrFetchError) {
89+
fragmentRes = await matchedFragment.onSsrFetchError(
90+
fragmentReq,
91+
fragmentFailedResOrError
92+
);
93+
} else {
94+
fragmentRes = new Response(
95+
mode === 'development'
96+
? `<p>Fetching fragment upstream failed: ${matchedFragment.upstream}</p>`
97+
: "<p>There was a problem fulfilling your request.</p>",
98+
{ headers: [["content-type", "text/html"]] }
99+
);
100+
}
101+
}
72102

73103
const rewriter = new HTMLRewriter()
74104
.on('head', {

0 commit comments

Comments
 (0)