Skip to content

Commit 1be5644

Browse files
authored
Add tracing to headers & redirects for Workers Assets (#9050)
1 parent 45973b2 commit 1be5644

File tree

4 files changed

+178
-107
lines changed

4 files changed

+178
-107
lines changed

.changeset/cyan-ends-melt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/workers-shared": patch
3+
---
4+
5+
Adds tracing to \_headers & \_redirects in Workers Assets allowing Cloudflare employees to better debug customer issues regarding these features.

packages/workers-shared/asset-worker/src/handler.ts

Lines changed: 92 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ import {
1111
SeeOtherResponse,
1212
TemporaryRedirectResponse,
1313
} from "../../utils/responses";
14+
import { mockJaegerBinding } from "../../utils/tracing";
1415
import {
1516
flagIsEnabled,
1617
SEC_FETCH_MODE_NAVIGATE_HEADER_PREFERS_ASSET_SERVING,
1718
} from "./compatibility-flags";
1819
import { attachCustomHeaders, getAssetHeaders } from "./utils/headers";
19-
import { generateRulesMatcher, replacer } from "./utils/rules-engine";
20+
import {
21+
generateRedirectsMatcher,
22+
staticRedirectsMatcher,
23+
} from "./utils/rules-engine";
2024
import type { AssetConfig } from "../../utils/types";
2125
import type { Analytics } from "./analytics";
2226
import type EntrypointType from "./index";
2327
import type { Env } from "./index";
2428

25-
const REDIRECTS_VERSION = 1;
29+
export const REDIRECTS_VERSION = 1;
2630
export const HEADERS_VERSION = 2;
2731

2832
type AssetIntent = {
@@ -39,77 +43,20 @@ const getResponseOrAssetIntent = async (
3943
exists: typeof EntrypointType.prototype.unstable_exists
4044
): Promise<Response | AssetIntentWithResolver> => {
4145
const url = new URL(request.url);
42-
const { host, search } = url;
43-
let { pathname } = url;
44-
45-
const staticRedirectsMatcher = () => {
46-
const withHostMatch =
47-
configuration.redirects.staticRules[`https://${host}${pathname}`];
48-
const withoutHostMatch = configuration.redirects.staticRules[pathname];
49-
50-
if (withHostMatch && withoutHostMatch) {
51-
if (withHostMatch.lineNumber < withoutHostMatch.lineNumber) {
52-
return withHostMatch;
53-
} else {
54-
return withoutHostMatch;
55-
}
56-
}
57-
58-
return withHostMatch || withoutHostMatch;
59-
};
60-
61-
const generateRedirectsMatcher = () =>
62-
generateRulesMatcher(
63-
configuration.redirects.version === REDIRECTS_VERSION
64-
? configuration.redirects.rules
65-
: {},
66-
({ status, to }, replacements) => ({
67-
status,
68-
to: replacer(to, replacements),
69-
})
70-
);
46+
const { search } = url;
7147

72-
const redirectMatch =
73-
staticRedirectsMatcher() || generateRedirectsMatcher()({ request })[0];
74-
75-
let proxied = false;
76-
77-
if (redirectMatch) {
78-
if (redirectMatch.status === 200) {
79-
// A 200 redirect means that we are proxying/rewriting to a different asset, for example,
80-
// a request with url /users/12345 could be pointed to /users/id.html. In order to
81-
// do this, we overwrite the pathname, and instead match for assets with that url,
82-
// and importantly, do not use the regular redirect handler - as the url visible to
83-
// the user does not change
84-
pathname = new URL(redirectMatch.to, request.url).pathname;
85-
proxied = true;
86-
} else {
87-
const { status, to } = redirectMatch;
88-
const destination = new URL(to, request.url);
89-
const location =
90-
destination.origin === new URL(request.url).origin
91-
? `${destination.pathname}${destination.search || search}${
92-
destination.hash
93-
}`
94-
: `${destination.href.slice(0, destination.href.length - (destination.search.length + destination.hash.length))}${
95-
destination.search ? destination.search : search
96-
}${destination.hash}`;
97-
98-
switch (status) {
99-
case MovedPermanentlyResponse.status:
100-
return new MovedPermanentlyResponse(location);
101-
case SeeOtherResponse.status:
102-
return new SeeOtherResponse(location);
103-
case TemporaryRedirectResponse.status:
104-
return new TemporaryRedirectResponse(location);
105-
case PermanentRedirectResponse.status:
106-
return new PermanentRedirectResponse(location);
107-
case FoundResponse.status:
108-
default:
109-
return new FoundResponse(location);
110-
}
111-
}
48+
const redirectResult = handleRedirects(
49+
env,
50+
request,
51+
configuration,
52+
url.host,
53+
url.pathname,
54+
search
55+
);
56+
if (redirectResult instanceof Response) {
57+
return redirectResult;
11258
}
59+
const { proxied, pathname } = redirectResult;
11360

11461
const decodedPathname = decodePath(pathname);
11562

@@ -306,7 +253,7 @@ export const handleRequest = async (
306253
analytics
307254
);
308255

309-
return attachCustomHeaders(request, response, configuration);
256+
return attachCustomHeaders(request, response, configuration, env);
310257
};
311258

312259
type Resolver = "html-handling" | "not-found";
@@ -1048,3 +995,76 @@ const encodePath = (pathname: string) => {
1048995
})
1049996
.join("/");
1050997
};
998+
999+
const handleRedirects = (
1000+
env: Env,
1001+
request: Request,
1002+
configuration: Required<AssetConfig>,
1003+
host: string,
1004+
pathname: string,
1005+
search: string
1006+
): { proxied: boolean; pathname: string } | Response => {
1007+
const jaeger = env.JAEGER ?? mockJaegerBinding();
1008+
return jaeger.enterSpan("handle_redirects", (span) => {
1009+
const redirectMatch =
1010+
staticRedirectsMatcher(configuration, host, pathname) ||
1011+
generateRedirectsMatcher(configuration)({ request })[0];
1012+
1013+
let proxied = false;
1014+
if (redirectMatch) {
1015+
if (redirectMatch.status === 200) {
1016+
// A 200 redirect means that we are proxying/rewriting to a different asset, for example,
1017+
// a request with url /users/12345 could be pointed to /users/id.html. In order to
1018+
// do this, we overwrite the pathname, and instead match for assets with that url,
1019+
// and importantly, do not use the regular redirect handler - as the url visible to
1020+
// the user does not change
1021+
pathname = new URL(redirectMatch.to, request.url).pathname;
1022+
proxied = true;
1023+
1024+
span.setTags({
1025+
matched: true,
1026+
proxied: true,
1027+
new_path: pathname,
1028+
status: redirectMatch.status,
1029+
});
1030+
} else {
1031+
const { status, to } = redirectMatch;
1032+
const destination = new URL(to, request.url);
1033+
const location =
1034+
destination.origin === new URL(request.url).origin
1035+
? `${destination.pathname}${destination.search || search}${
1036+
destination.hash
1037+
}`
1038+
: `${destination.href.slice(0, destination.href.length - (destination.search.length + destination.hash.length))}${
1039+
destination.search ? destination.search : search
1040+
}${destination.hash}`;
1041+
1042+
span.setTags({
1043+
matched: true,
1044+
destination: location,
1045+
status,
1046+
});
1047+
1048+
switch (status) {
1049+
case MovedPermanentlyResponse.status:
1050+
return new MovedPermanentlyResponse(location);
1051+
case SeeOtherResponse.status:
1052+
return new SeeOtherResponse(location);
1053+
case TemporaryRedirectResponse.status:
1054+
return new TemporaryRedirectResponse(location);
1055+
case PermanentRedirectResponse.status:
1056+
return new PermanentRedirectResponse(location);
1057+
case FoundResponse.status:
1058+
default:
1059+
return new FoundResponse(location);
1060+
}
1061+
}
1062+
} else {
1063+
span.setTags({
1064+
matched: false,
1065+
});
1066+
}
1067+
1068+
return { proxied, pathname };
1069+
});
1070+
};

packages/workers-shared/asset-worker/src/utils/headers.ts

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { mockJaegerBinding } from "../../../utils/tracing";
12
import {
23
flagIsEnabled,
34
SEC_FETCH_MODE_NAVIGATE_HEADER_PREFERS_ASSET_SERVING,
45
} from "../compatibility-flags";
56
import { CACHE_CONTROL_BROWSER } from "../constants";
67
import { HEADERS_VERSION } from "../handler";
78
import { generateRulesMatcher, replacer } from "./rules-engine";
9+
import type { Env } from "..";
810
import type { AssetConfig } from "../../../utils/types";
911
import type { AssetIntentWithResolver } from "../handler";
1012

@@ -61,44 +63,51 @@ function isCacheable(request: Request) {
6163
export function attachCustomHeaders(
6264
request: Request,
6365
response: Response,
64-
configuration: Required<AssetConfig>
66+
configuration: Required<AssetConfig>,
67+
env: Env
6568
) {
66-
// Iterate through rules and find rules that match the path
67-
const headersMatcher = generateRulesMatcher(
68-
configuration.headers?.version === HEADERS_VERSION
69-
? configuration.headers.rules
70-
: {},
71-
({ set = {}, unset = [] }, replacements) => {
72-
const replacedSet: Record<string, string> = {};
69+
const jaeger = env.JAEGER ?? mockJaegerBinding();
70+
return jaeger.enterSpan("add_headers", (span) => {
71+
// Iterate through rules and find rules that match the path
72+
const headersMatcher = generateRulesMatcher(
73+
configuration.headers?.version === HEADERS_VERSION
74+
? configuration.headers.rules
75+
: {},
76+
({ set = {}, unset = [] }, replacements) => {
77+
const replacedSet: Record<string, string> = {};
78+
Object.keys(set).forEach((key) => {
79+
replacedSet[key] = replacer(set[key], replacements);
80+
});
81+
return {
82+
set: replacedSet,
83+
unset,
84+
};
85+
}
86+
);
87+
const matches = headersMatcher({ request });
88+
89+
// This keeps track of every header that we've set from _headers
90+
// because we want to combine user declared headers but overwrite
91+
// existing and extra ones
92+
const setMap = new Set();
93+
// Apply every matched rule in order
94+
matches.forEach(({ set = {}, unset = [] }) => {
95+
unset.forEach((key) => {
96+
response.headers.delete(key);
97+
span.addLogs({ remove_header: key });
98+
});
7399
Object.keys(set).forEach((key) => {
74-
replacedSet[key] = replacer(set[key], replacements);
100+
if (setMap.has(key.toLowerCase())) {
101+
response.headers.append(key, set[key]);
102+
span.addLogs({ append_header: key });
103+
} else {
104+
response.headers.set(key, set[key]);
105+
setMap.add(key.toLowerCase());
106+
span.addLogs({ add_header: key });
107+
}
75108
});
76-
return {
77-
set: replacedSet,
78-
unset,
79-
};
80-
}
81-
);
82-
const matches = headersMatcher({ request });
83-
84-
// This keeps track of every header that we've set from _headers
85-
// because we want to combine user declared headers but overwrite
86-
// existing and extra ones
87-
const setMap = new Set();
88-
// Apply every matched rule in order
89-
matches.forEach(({ set = {}, unset = [] }) => {
90-
unset.forEach((key) => {
91-
response.headers.delete(key);
92-
});
93-
Object.keys(set).forEach((key) => {
94-
if (setMap.has(key.toLowerCase())) {
95-
response.headers.append(key, set[key]);
96-
} else {
97-
response.headers.set(key, set[key]);
98-
setMap.add(key.toLowerCase());
99-
}
100109
});
101-
});
102110

103-
return response;
111+
return response;
112+
});
104113
}

packages/workers-shared/asset-worker/src/utils/rules-engine.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Taken from https://stackoverflow.com/a/3561711
22
// which is everything from the tc39 proposal, plus the following two characters: ^/
33
// It's also everything included in the URLPattern escape (https://wicg.github.io/urlpattern/#escape-a-regexp-string), plus the following: -
4+
5+
import { REDIRECTS_VERSION } from "../handler";
6+
import type { AssetConfig } from "../../../utils/types";
7+
48
// As the answer says, there's no downside to escaping these extra characters, so better safe than sorry
59
const ESCAPE_REGEX_CHARACTERS = /[-/\\^$*+?.()|[\]{}]/g;
610
const escapeRegex = (str: string) => {
@@ -94,3 +98,36 @@ export const generateRulesMatcher = <T>(
9498
.filter((value) => value !== undefined) as T[];
9599
};
96100
};
101+
102+
export const staticRedirectsMatcher = (
103+
configuration: Required<AssetConfig>,
104+
host: string,
105+
pathname: string
106+
) => {
107+
const withHostMatch =
108+
configuration.redirects.staticRules[`https://${host}${pathname}`];
109+
const withoutHostMatch = configuration.redirects.staticRules[pathname];
110+
111+
if (withHostMatch && withoutHostMatch) {
112+
if (withHostMatch.lineNumber < withoutHostMatch.lineNumber) {
113+
return withHostMatch;
114+
} else {
115+
return withoutHostMatch;
116+
}
117+
}
118+
119+
return withHostMatch || withoutHostMatch;
120+
};
121+
122+
export const generateRedirectsMatcher = (
123+
configuration: Required<AssetConfig>
124+
) =>
125+
generateRulesMatcher(
126+
configuration.redirects.version === REDIRECTS_VERSION
127+
? configuration.redirects.rules
128+
: {},
129+
({ status, to }, replacements) => ({
130+
status,
131+
to: replacer(to, replacements),
132+
})
133+
);

0 commit comments

Comments
 (0)