Skip to content

Commit f88280f

Browse files
committed
fix: copy search params when calling beacon client
Fix #15818
1 parent 4660801 commit f88280f

File tree

3 files changed

+223
-34
lines changed

3 files changed

+223
-34
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { SecretValue } from '@aztec/foundation/config';
2+
3+
import { getBeaconNodeFetchOptions } from './beacon_api.js';
4+
import type { BlobSinkConfig } from './config.js';
5+
6+
describe('getBeaconNodeFetchOptions', () => {
7+
const mockConfig: BlobSinkConfig = {};
8+
9+
describe('URL construction', () => {
10+
it('should construct URL from string base URL', () => {
11+
const result = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', mockConfig);
12+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs');
13+
});
14+
15+
it('should handle base URLs with paths - absolute API replaces path', () => {
16+
const result = getBeaconNodeFetchOptions('http://localhost:3000/base', '/api/v1/blobs', mockConfig);
17+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs');
18+
});
19+
20+
it('should handle base URLs with paths - relative API appends to path', () => {
21+
const result = getBeaconNodeFetchOptions('http://localhost:3000/base/', 'api/v1/blobs', mockConfig);
22+
expect(result.url.href).toBe('http://localhost:3000/base/api/v1/blobs');
23+
});
24+
});
25+
26+
describe('search params preservation', () => {
27+
it('should preserve search params from string base URL', () => {
28+
const result = getBeaconNodeFetchOptions(
29+
'http://localhost:3000?existing=value&another=param',
30+
'/api/v1/blobs',
31+
mockConfig,
32+
);
33+
34+
expect(result.url.searchParams.get('existing')).toBe('value');
35+
expect(result.url.searchParams.get('another')).toBe('param');
36+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs?existing=value&another=param');
37+
});
38+
});
39+
40+
describe('API key as query parameter', () => {
41+
it('should add API key as query parameter when no header is specified', () => {
42+
const config: BlobSinkConfig = {
43+
l1ConsensusHostApiKeys: [new SecretValue('test-api-key')],
44+
};
45+
46+
const result = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 0);
47+
48+
expect(result.url.searchParams.get('key')).toBe('test-api-key');
49+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs?key=test-api-key');
50+
expect(result.headers).toBeUndefined();
51+
});
52+
53+
it('should add API key to existing search params', () => {
54+
const config: BlobSinkConfig = {
55+
l1ConsensusHostApiKeys: [new SecretValue('test-api-key')],
56+
};
57+
58+
const result = getBeaconNodeFetchOptions('http://localhost:3000?existing=value', '/api/v1/blobs', config, 0);
59+
60+
expect(result.url.searchParams.get('existing')).toBe('value');
61+
expect(result.url.searchParams.get('key')).toBe('test-api-key');
62+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs?existing=value&key=test-api-key');
63+
});
64+
65+
it('should use correct API key based on index', () => {
66+
const config: BlobSinkConfig = {
67+
l1ConsensusHostApiKeys: [
68+
new SecretValue('first-key'),
69+
new SecretValue('second-key'),
70+
new SecretValue('third-key'),
71+
],
72+
};
73+
74+
const result1 = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 0);
75+
expect(result1.url.searchParams.get('key')).toBe('first-key');
76+
77+
const result2 = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 1);
78+
expect(result2.url.searchParams.get('key')).toBe('second-key');
79+
80+
const result3 = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 2);
81+
expect(result3.url.searchParams.get('key')).toBe('third-key');
82+
});
83+
});
84+
85+
describe('API key as header', () => {
86+
it('should add API key as header when header name is specified', () => {
87+
const config: BlobSinkConfig = {
88+
l1ConsensusHostApiKeys: [new SecretValue('test-api-key')],
89+
l1ConsensusHostApiKeyHeaders: ['X-API-Key'],
90+
};
91+
92+
const result = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 0);
93+
94+
expect(result.url.searchParams.has('key')).toBe(false);
95+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs');
96+
expect(result.headers).toEqual({
97+
'X-API-Key': 'test-api-key',
98+
});
99+
});
100+
101+
it('should use correct header name and API key based on index', () => {
102+
const config: BlobSinkConfig = {
103+
l1ConsensusHostApiKeys: [new SecretValue('first-key'), new SecretValue('second-key')],
104+
l1ConsensusHostApiKeyHeaders: ['X-API-Key-1', 'X-API-Key-2'],
105+
};
106+
107+
const result1 = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 0);
108+
expect(result1.headers).toEqual({
109+
'X-API-Key-1': 'first-key',
110+
});
111+
112+
const result2 = getBeaconNodeFetchOptions('http://localhost:3000', '/api/v1/blobs', config, 1);
113+
expect(result2.headers).toEqual({
114+
'X-API-Key-2': 'second-key',
115+
});
116+
});
117+
118+
it('should preserve existing search params when using headers', () => {
119+
const config: BlobSinkConfig = {
120+
l1ConsensusHostApiKeys: [new SecretValue('test-api-key')],
121+
l1ConsensusHostApiKeyHeaders: ['Authorization'],
122+
};
123+
124+
const result = getBeaconNodeFetchOptions('http://localhost:3000?existing=value', '/api/v1/blobs', config, 0);
125+
126+
expect(result.url.searchParams.get('existing')).toBe('value');
127+
expect(result.url.searchParams.has('key')).toBe(false);
128+
expect(result.url.href).toBe('http://localhost:3000/api/v1/blobs?existing=value');
129+
expect(result.headers).toEqual({
130+
Authorization: 'test-api-key',
131+
});
132+
});
133+
});
134+
135+
describe('edge cases', () => {
136+
it('should handle URLs with special characters', () => {
137+
const result = getBeaconNodeFetchOptions(
138+
'http://localhost:3000?query=hello%20world',
139+
'/api/v1/blobs',
140+
mockConfig,
141+
);
142+
143+
expect(result.url.searchParams.get('query')).toBe('hello world');
144+
});
145+
146+
it('should handle complex URL combinations', () => {
147+
const config: BlobSinkConfig = {
148+
l1ConsensusHostApiKeys: [new SecretValue('complex-key')],
149+
l1ConsensusHostApiKeyHeaders: ['Authorization'],
150+
};
151+
152+
const result = getBeaconNodeFetchOptions(
153+
'https://api.example.com:8080/base?existing=value&another=test',
154+
'/beacon/api/v1/blobs',
155+
config,
156+
0,
157+
);
158+
159+
expect(result.url.href).toBe('https://api.example.com:8080/beacon/api/v1/blobs?existing=value&another=test');
160+
expect(result.headers).toEqual({
161+
Authorization: 'complex-key',
162+
});
163+
});
164+
});
165+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { BlobSinkConfig } from './config.js';
2+
3+
export function getBeaconNodeFetchOptions(
4+
_baseUrl: string | URL,
5+
api: string,
6+
config: BlobSinkConfig,
7+
l1ConsensusHostIndex?: number,
8+
): {
9+
url: URL;
10+
headers?: Record<string, string>;
11+
} {
12+
const { l1ConsensusHostApiKeys, l1ConsensusHostApiKeyHeaders } = config;
13+
const l1ConsensusHostApiKey =
14+
l1ConsensusHostIndex !== undefined && l1ConsensusHostApiKeys && l1ConsensusHostApiKeys[l1ConsensusHostIndex];
15+
const l1ConsensusHostApiKeyHeader =
16+
l1ConsensusHostIndex !== undefined &&
17+
l1ConsensusHostApiKeyHeaders &&
18+
l1ConsensusHostApiKeyHeaders[l1ConsensusHostIndex];
19+
20+
const baseUrl = typeof _baseUrl === 'string' ? new URL(_baseUrl) : _baseUrl;
21+
const url = new URL(api, baseUrl);
22+
23+
if (baseUrl.searchParams.size > 0) {
24+
for (const [key, value] of baseUrl.searchParams.entries()) {
25+
url.searchParams.append(key, value);
26+
}
27+
}
28+
29+
if (l1ConsensusHostApiKey && !l1ConsensusHostApiKeyHeader) {
30+
url.searchParams.set('key', l1ConsensusHostApiKey.getValue());
31+
}
32+
33+
return {
34+
url,
35+
...(l1ConsensusHostApiKey &&
36+
l1ConsensusHostApiKeyHeader && {
37+
headers: {
38+
[l1ConsensusHostApiKeyHeader]: l1ConsensusHostApiKey.getValue(),
39+
},
40+
}),
41+
};
42+
}

yarn-project/blob-sink/src/client/http.ts

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createBlobArchiveClient } from '../archive/factory.js';
99
import type { BlobArchiveClient } from '../archive/interface.js';
1010
import { outboundTransform } from '../encoding/index.js';
1111
import { BlobWithIndex } from '../types/blob_with_index.js';
12+
import { getBeaconNodeFetchOptions } from './beacon_api.js';
1213
import { type BlobSinkConfig, getBlobSinkConfigFromEnv } from './config.js';
1314
import type { BlobSinkClientInterface } from './interface.js';
1415

@@ -71,11 +72,12 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface {
7172
const l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
7273
try {
7374
const { url, ...options } = getBeaconNodeFetchOptions(
74-
`${l1ConsensusHostUrl}/eth/v1/beacon/headers`,
75+
l1ConsensusHostUrl,
76+
'eth/v1/beacon/headers',
7577
this.config,
7678
l1ConsensusHostIndex,
7779
);
78-
const res = await this.fetch(url, options);
80+
const res = await this.fetch(url.href, options);
7981
if (res.ok) {
8082
this.log.info(`L1 consensus host is reachable`, { l1ConsensusHostUrl });
8183
successfulSourceCount++;
@@ -299,22 +301,26 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface {
299301
indices: number[],
300302
l1ConsensusHostIndex?: number,
301303
): Promise<Response> {
302-
let baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
304+
let baseUrl = `eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`;
303305
if (indices.length > 0) {
304306
baseUrl += `?indices=${indices.join(',')}`;
305307
}
306308

307-
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
309+
const { url, ...options } = getBeaconNodeFetchOptions(hostUrl, baseUrl, this.config, l1ConsensusHostIndex);
308310
this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options });
309-
return this.fetch(url, options);
311+
return this.fetch(url.href, options);
310312
}
311313

312314
private async getLatestSlotNumber(hostUrl: string, l1ConsensusHostIndex?: number): Promise<number | undefined> {
313315
try {
314-
const baseUrl = `${hostUrl}/eth/v1/beacon/headers/head`;
315-
const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex);
316+
const { url, ...options } = getBeaconNodeFetchOptions(
317+
hostUrl,
318+
'eth/v1/beacon/headers/head',
319+
this.config,
320+
l1ConsensusHostIndex,
321+
);
316322
this.log.debug(`Fetching latest slot number`, { url, ...options });
317-
const res = await this.fetch(url, options);
323+
const res = await this.fetch(url.href, options);
318324
if (res.ok) {
319325
const body = await res.json();
320326
const slot = parseInt(body.data.header.message.slot);
@@ -385,11 +391,12 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface {
385391
l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex];
386392
try {
387393
const { url, ...options } = getBeaconNodeFetchOptions(
394+
l1ConsensusHostUrl,
388395
`${l1ConsensusHostUrl}/eth/v1/beacon/headers/${parentBeaconBlockRoot}`,
389396
this.config,
390397
l1ConsensusHostIndex,
391398
);
392-
const res = await this.fetch(url, options);
399+
const res = await this.fetch(url.href, options);
393400

394401
if (res.ok) {
395402
const body = await res.json();
@@ -442,28 +449,3 @@ async function getRelevantBlobs(
442449
const maybeBlobs = await Promise.all(blobsPromise);
443450
return maybeBlobs.filter((b: BlobWithIndex | undefined): b is BlobWithIndex => b !== undefined);
444451
}
445-
446-
function getBeaconNodeFetchOptions(url: string, config: BlobSinkConfig, l1ConsensusHostIndex?: number) {
447-
const { l1ConsensusHostApiKeys, l1ConsensusHostApiKeyHeaders } = config;
448-
const l1ConsensusHostApiKey =
449-
l1ConsensusHostIndex !== undefined && l1ConsensusHostApiKeys && l1ConsensusHostApiKeys[l1ConsensusHostIndex];
450-
const l1ConsensusHostApiKeyHeader =
451-
l1ConsensusHostIndex !== undefined &&
452-
l1ConsensusHostApiKeyHeaders &&
453-
l1ConsensusHostApiKeyHeaders[l1ConsensusHostIndex];
454-
455-
let formattedUrl = url;
456-
if (l1ConsensusHostApiKey && !l1ConsensusHostApiKeyHeader) {
457-
formattedUrl += `${formattedUrl.includes('?') ? '&' : '?'}key=${l1ConsensusHostApiKey.getValue()}`;
458-
}
459-
460-
return {
461-
url: formattedUrl,
462-
...(l1ConsensusHostApiKey &&
463-
l1ConsensusHostApiKeyHeader && {
464-
headers: {
465-
[l1ConsensusHostApiKeyHeader]: l1ConsensusHostApiKey.getValue(),
466-
},
467-
}),
468-
};
469-
}

0 commit comments

Comments
 (0)