Skip to content

Commit ee0ec17

Browse files
authored
No redirects in CDN Worker (#5540)
1 parent 8b3244e commit ee0ec17

File tree

19 files changed

+78
-165
lines changed

19 files changed

+78
-165
lines changed

docker/docker-compose.community.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ services:
226226
S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
227227
S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
228228
S3_BUCKET_NAME: artifacts
229-
S3_PUBLIC_URL: 'http://localhost:8083'
230229
CDN_AUTH_PRIVATE_KEY: ${CDN_AUTH_PRIVATE_KEY}
231230
CDN_API: '1'
232231
CDN_API_BASE_URL: 'http://localhost:8082'

integration-tests/docker-compose.integration.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ services:
3636
S3_ACCESS_KEY_ID: minioadmin
3737
S3_SECRET_ACCESS_KEY: minioadmin
3838
S3_BUCKET_NAME: artifacts
39-
S3_PUBLIC_URL: 'http://localhost:8083'
4039

4140
local_broker:
4241
image: node:22.6.0-alpine3.20
@@ -169,7 +168,6 @@ services:
169168
GITHUB_APP_PRIVATE_KEY: 5f938d51a065476c4dc1b04aeba13afb
170169
FEEDBACK_SLACK_TOKEN: ''
171170
FEEDBACK_SLACK_CHANNEL: '#hive'
172-
S3_PUBLIC_URL: 'http://localhost:8083'
173171
USAGE_ESTIMATOR_ENDPOINT: '${USAGE_ESTIMATOR_ENDPOINT}'
174172
RATE_LIMIT_ENDPOINT: '${RATE_LIMIT_ENDPOINT}'
175173
EMAIL_PROVIDER: '${EMAIL_PROVIDER}'

integration-tests/tests/api/artifacts-cdn.spec.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createSupergraphManager } from '@graphql-hive/apollo';
1414
import { graphql } from '../../testkit/gql';
1515
import { execute } from '../../testkit/graphql';
1616
import { initSeed } from '../../testkit/seed';
17-
import { getCDNPort, getServiceHost, KnownServices } from '../../testkit/utils';
17+
import { getServiceHost, KnownServices } from '../../testkit/utils';
1818

1919
const s3Client = new S3Client({
2020
endpoint: 'http://127.0.0.1:9000',
@@ -250,9 +250,13 @@ function runArtifactsCDNTests(
250250
redirect: 'manual',
251251
});
252252

253-
expect(response.status).toBe(302);
254-
expect(await response.text()).toBe('Found.');
255-
expect(response.headers.get('location')).toBeDefined();
253+
expect(response.status).toBe(200);
254+
const body = await response.text();
255+
expect(body).toMatchInlineSnapshot(`
256+
type Query {
257+
ping: String
258+
}
259+
`);
256260

257261
const artifactContents = await fetchS3ObjectArtifact(
258262
'artifacts',
@@ -306,21 +310,8 @@ function runArtifactsCDNTests(
306310
redirect: 'manual',
307311
});
308312

309-
expect(response.status).toBe(302);
310-
expect(await response.text()).toBe('Found.');
311-
const locationHeader = response.headers.get('location');
312-
expect(locationHeader).toBeDefined();
313-
const locationUrl = new URL(locationHeader!);
314-
expect(locationUrl.protocol).toBe('http:');
315-
expect(locationUrl.hostname).toBe('localhost');
316-
expect(locationUrl.port).toBe(getCDNPort().toString());
317-
318-
response = await fetch(locationHeader!, {
319-
method: 'GET',
320-
redirect: 'manual',
321-
});
322-
const body = await response.text();
323313
expect(response.status).toBe(200);
314+
const body = await response.text();
324315
expect(body).toMatchInlineSnapshot(
325316
'[{"name":"ping","sdl":"type Query { ping: String }","url":"http://ping.com"}]',
326317
);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const version = '0.33.4';
1+
export const version = '0.36.0';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const version = '0.7.0';
1+
export const version = '0.8.0';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const version = '0.33.3';
1+
export const version = '0.33.8';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const version = '0.33.3';
1+
export const version = '0.37.0';
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
S3_ENDPOINT=http://localhost:9000
22
S3_ACCESS_KEY_ID="minioadmin"
33
S3_SECRET_ACCESS_KEY="minioadmin"
4-
S3_BUCKET_NAME="artifacts"
5-
S3_PUBLIC_URL="http://localhost:9000"
4+
S3_BUCKET_NAME="artifacts"

packages/services/cdn-worker/src/artifact-handler.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as itty from 'itty-router';
22
import zod from 'zod';
33
import { createAnalytics, type Analytics } from './analytics';
44
import { type ArtifactStorageReader, type ArtifactsType } from './artifact-storage-reader';
5-
import { InvalidAuthKeyResponse, MissingAuthKeyResponse, UnexpectedError } from './errors';
5+
import { InvalidAuthKeyResponse, MissingAuthKeyResponse } from './errors';
66
import { IsAppDeploymentActive } from './is-app-deployment-active';
77
import type { KeyValidator } from './key-validation';
88
import { createResponse } from './tracked-response';
@@ -13,16 +13,7 @@ export type GetArtifactActionFn = (
1313
artifactType: ArtifactsType,
1414
eTag: string | null,
1515
) => Promise<
16-
| { type: 'notModified' }
17-
| { type: 'notFound' }
18-
| { type: 'body'; body: string }
19-
| {
20-
type: 'redirect';
21-
location: {
22-
public: string;
23-
private: string;
24-
};
25-
}
16+
{ type: 'notModified' } | { type: 'notFound' } | { type: 'response'; response: Response }
2617
>;
2718

2819
type ArtifactRequestHandler = {
@@ -161,7 +152,8 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
161152
return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId, request);
162153
}
163154

164-
if (result.type === 'redirect') {
155+
if (result.type === 'response') {
156+
const etag = result.response.headers.get('etag');
165157
if (params.artifactType === 'metadata') {
166158
// To not change a lot of logic and still reuse the etag bits, we
167159
// fetch metadata using the redirect location.
@@ -171,19 +163,8 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
171163
// We're using here a private location, because the public S3 endpoint may differ from the internal S3 endpoint. E.g. within a docker network,
172164
// and we're fetching the artifact from within the private network.
173165
// If they are the same, private and public locations will be the same.
174-
const metadataResponse = await fetch(result.location.private);
175-
176-
if (!metadataResponse.ok) {
177-
console.error(
178-
'Failed to fetch metadata',
179-
metadataResponse.status,
180-
metadataResponse.statusText,
181-
);
182166

183-
return new UnexpectedError(analytics, request);
184-
}
185-
186-
const body = await metadataResponse.text();
167+
const body = await result.response.clone().text();
187168

188169
// Metadata in SINGLE projects is only Mesh's Metadata, and it always defines _schema
189170
const isMeshArtifact = body.includes(`"#/definitions/_schema"`);
@@ -192,7 +173,6 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
192173
// Mesh's Metadata shared by Mesh is always an object.
193174
// The top-level array was caused #3291 and fixed now, but we still need to handle the old data.
194175
if (isMeshArtifact && hasTopLevelArray) {
195-
const etag = metadataResponse.headers.get('etag');
196176
return createResponse(
197177
analytics,
198178
body.substring(1, body.length - 1),
@@ -211,11 +191,17 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
211191

212192
return createResponse(
213193
analytics,
214-
'Found.',
215-
// We're using here a public location, because we expose the Location to the end user and
216-
// the public S3 endpoint may differ from the internal S3 endpoint. E.g. within a docker network.
217-
// If they are the same, private and public locations will be the same.
218-
{ status: 302, headers: { Location: result.location.public } },
194+
await result.response.text(),
195+
{
196+
status: 200,
197+
headers: {
198+
'Content-Type':
199+
params.artifactType === 'metadata' || params.artifactType === 'services'
200+
? 'application/json'
201+
: 'text/plain',
202+
...(etag ? { etag } : {}),
203+
},
204+
},
219205
params.targetId,
220206
request,
221207
);

packages/services/cdn-worker/src/artifact-storage-reader.ts

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import zod from 'zod';
22
import type { Analytics } from './analytics';
33
import { AwsClient } from './aws';
44

5-
const presignedUrlExpirationSeconds = 60;
6-
75
export function buildArtifactStorageKey(
86
targetId: string,
97
artifactType: string,
@@ -61,54 +59,14 @@ export function buildAppDeploymentIsEnabledKey(
6159
* Read an artifact/app deployment operation from S3.
6260
*/
6361
export class ArtifactStorageReader {
64-
private publicUrl: URL | null;
65-
6662
constructor(
6763
private s3: {
6864
client: AwsClient;
6965
endpoint: string;
7066
bucketName: string;
7167
},
72-
/** The public URL in case the public S3 endpoint differs from the internal S3 endpoint. E.g. within a docker network. */
73-
publicUrl: string | null,
7468
private analytics: Analytics | null,
75-
) {
76-
this.publicUrl = publicUrl ? new URL(publicUrl) : null;
77-
}
78-
79-
private async generatePresignedGetUrl(key: string): Promise<{
80-
public: string;
81-
private: string;
82-
}> {
83-
const [signedUrl] = await this.s3.client.sign(
84-
[this.s3.endpoint, this.s3.bucketName, key].join('/'),
85-
{
86-
method: 'GET',
87-
aws: { signQuery: true },
88-
headers: {
89-
'X-Amz-Expires': String(presignedUrlExpirationSeconds),
90-
},
91-
timeout: READ_TIMEOUT_MS,
92-
},
93-
);
94-
95-
if (!this.publicUrl) {
96-
return {
97-
public: signedUrl,
98-
private: signedUrl,
99-
};
100-
}
101-
102-
const publicUrl = new URL(signedUrl);
103-
publicUrl.protocol = this.publicUrl.protocol;
104-
publicUrl.host = this.publicUrl.host;
105-
publicUrl.port = this.publicUrl.port;
106-
107-
return {
108-
public: publicUrl.toString(),
109-
private: signedUrl,
110-
};
111-
}
69+
) {}
11270

11371
/** Generate a pre-signed url for reading an artifact from a bucket for a limited time period. */
11472
async generateArtifactReadUrl(
@@ -123,7 +81,7 @@ export class ArtifactStorageReader {
12381

12482
const key = buildArtifactStorageKey(targetId, artifactType, contractName);
12583

126-
const response = await this.s3.client.fetch(
84+
const headResponse = await this.s3.client.fetch(
12785
[this.s3.endpoint, this.s3.bucketName, key].join('/'),
12886
{
12987
method: 'HEAD',
@@ -136,27 +94,42 @@ export class ArtifactStorageReader {
13694
this.analytics?.track(
13795
{
13896
type: 'r2',
139-
statusCode: response.status,
97+
statusCode: headResponse.status,
14098
action: 'HEAD artifact',
14199
},
142100
targetId,
143101
);
144102

145-
if (response.status === 200) {
146-
if (etagValue && response.headers.get('etag') === etagValue) {
103+
if (headResponse.status === 200) {
104+
if (etagValue && headResponse.headers.get('etag') === etagValue) {
147105
return { type: 'notModified' } as const;
148106
}
149107

150-
return {
151-
type: 'redirect',
152-
location: await this.generatePresignedGetUrl(key),
153-
} as const;
108+
const getResponse = await this.s3.client.fetch(
109+
[this.s3.endpoint, this.s3.bucketName, key].join('/'),
110+
{
111+
method: 'GET',
112+
aws: {
113+
signQuery: true,
114+
},
115+
timeout: READ_TIMEOUT_MS,
116+
},
117+
);
118+
119+
if (getResponse.ok) {
120+
return {
121+
type: 'response',
122+
response: getResponse,
123+
} as const;
124+
}
125+
126+
throw new Error(`GET request failed with status ${getResponse.status}`);
154127
}
155-
if (response.status === 404) {
128+
if (headResponse.status === 404) {
156129
return { type: 'notFound' } as const;
157130
}
158-
const body = await response.text();
159-
throw new Error(`HEAD request failed with status ${response.status}: ${body}`);
131+
const body = await headResponse.text();
132+
throw new Error(`HEAD request failed with status ${headResponse.status}: ${body}`);
160133
}
161134

162135
async isAppDeploymentEnabled(targetId: string, appName: string, appVersion: string) {

0 commit comments

Comments
 (0)