Skip to content

Commit f88050c

Browse files
fix: user-agent must be uri safe
1 parent 700a636 commit f88050c

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import assert from 'node:assert/strict';
2+
import { createRequire } from 'node:module';
3+
import { afterEach, describe, it } from 'node:test';
4+
5+
const require = createRequire(import.meta.url);
6+
7+
function isLatin1Safe(s: string): boolean {
8+
return Buffer.from(s, 'latin1').toString('latin1') === s;
9+
}
10+
11+
describe('instrument', () => {
12+
const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'title');
13+
const modulePath = require.resolve('./instrument.ts');
14+
15+
afterEach(() => {
16+
// Restore the original process.title property
17+
if (originalDescriptor) {
18+
Object.defineProperty(process, 'title', originalDescriptor);
19+
}
20+
// Clear module cache so next require gets a fresh evaluation
21+
delete require.cache[modulePath];
22+
});
23+
24+
function mockProcessTitle(title: string): void {
25+
Object.defineProperty(process, 'title', {
26+
get: () => title,
27+
configurable: true,
28+
});
29+
}
30+
31+
/**
32+
* Returns a fresh import of the instrument module. Since `baseUserAgent` is computed at module
33+
* load time, we clear the require cache and re-require to pick up a mocked `process.title`.
34+
*/
35+
function freshImport(): typeof import('./instrument') {
36+
delete require.cache[modulePath];
37+
return require(modulePath);
38+
}
39+
40+
describe('getUserAgent', () => {
41+
it('should contain the package name', () => {
42+
const { getUserAgent } = freshImport();
43+
const ua = getUserAgent();
44+
assert.ok(ua.includes('@slack:web-api'), `User-Agent should contain @slack:web-api: ${ua}`);
45+
});
46+
47+
it('should include an ASCII process.title in the user agent', () => {
48+
mockProcessTitle('node');
49+
const { getUserAgent } = freshImport();
50+
const ua = getUserAgent();
51+
assert.ok(ua.includes('node/'), `User-Agent should contain node/: ${ua}`);
52+
assert.ok(isLatin1Safe(ua));
53+
});
54+
55+
it('should include other ASCII process.title in the user agent', () => {
56+
mockProcessTitle('openclaw-gateway');
57+
const { getUserAgent } = freshImport();
58+
const ua = getUserAgent();
59+
assert.ok(ua.includes('openclaw-gateway/'), `User-Agent should contain openclaw-gateway/: ${ua}`);
60+
assert.ok(isLatin1Safe(ua));
61+
});
62+
63+
it('should return a Latin-1 safe user agent when process.title contains non-ASCII characters', () => {
64+
const notLatin1SafeTitle = '管理者'
65+
assert.strictEqual(isLatin1Safe(notLatin1SafeTitle), false);
66+
67+
mockProcessTitle(notLatin1SafeTitle);
68+
const { getUserAgent } = freshImport();
69+
const ua = getUserAgent();
70+
assert.ok(isLatin1Safe(ua), `User-Agent contains non-Latin-1 characters: ${ua}`);
71+
assert.ok(!ua.includes(notLatin1SafeTitle), 'User-Agent should not contain raw non-ASCII characters');
72+
});
73+
});
74+
});

packages/web-api/src/instrument.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function replaceSlashes(s: string): string {
1818
// based code will report "browser/undefined" from a deno runtime.
1919
const baseUserAgent =
2020
`${replaceSlashes(packageJson.name)}/${packageJson.version} ` +
21-
`${basename(process.title)}/${process.version.replace('v', '')} ` +
21+
`${encodeURI(basename(process.title))}/${process.version.replace('v', '')} ` +
2222
`${os.platform()}/${os.release()}`;
2323

2424
const appMetadata: { [key: string]: string } = {};

0 commit comments

Comments
 (0)