Skip to content

Commit 943af34

Browse files
stainless-botRobertCraigie
authored andcommitted
fix(client)!: uri encode path parameters
chore: unknown commit message
1 parent 78f7b2c commit 943af34

File tree

6 files changed

+395
-9
lines changed

6 files changed

+395
-9
lines changed

src/internal/utils/path.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { AnthropicError } from '../../error';
2+
3+
/**
4+
* Percent-encode everything that isn't safe to have in a path without encoding safe chars.
5+
*
6+
* Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3:
7+
* > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
8+
* > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
9+
* > pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
10+
*/
11+
export function encodeURIPath(str: string) {
12+
return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
13+
}
14+
15+
export const createPathTagFunction = (pathEncoder = encodeURIPath) =>
16+
function path(statics: readonly string[], ...params: readonly unknown[]): string {
17+
// If there are no params, no processing is needed.
18+
if (statics.length === 1) return statics[0]!;
19+
20+
let postPath = false;
21+
const path = statics.reduce((previousValue, currentValue, index) => {
22+
if (/[?#]/.test(currentValue)) {
23+
postPath = true;
24+
}
25+
return (
26+
previousValue +
27+
currentValue +
28+
(index === params.length ? '' : (postPath ? encodeURIComponent : pathEncoder)(String(params[index])))
29+
);
30+
}, '');
31+
32+
const pathOnly = path.split(/[?#]/, 1)[0]!;
33+
const invalidSegments = [];
34+
const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
35+
let match;
36+
37+
// Find all invalid segments
38+
while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
39+
invalidSegments.push({
40+
start: match.index,
41+
length: match[0].length,
42+
});
43+
}
44+
45+
if (invalidSegments.length > 0) {
46+
let lastEnd = 0;
47+
const underline = invalidSegments.reduce((acc, segment) => {
48+
const spaces = ' '.repeat(segment.start - lastEnd);
49+
const arrows = '^'.repeat(segment.length);
50+
lastEnd = segment.start + segment.length;
51+
return acc + spaces + arrows;
52+
}, '');
53+
54+
throw new AnthropicError(
55+
`Path parameters result in path with invalid segments:\n${path}\n${underline}`,
56+
);
57+
}
58+
59+
return path;
60+
};
61+
62+
/**
63+
* URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
64+
*/
65+
export const path = createPathTagFunction(encodeURIPath);

src/resources/beta/messages/batches.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { buildHeaders } from '../../../internal/headers';
99
import { RequestOptions } from '../../../internal/request-options';
1010
import { JSONLDecoder } from '../../../internal/decoders/jsonl';
1111
import { AnthropicError } from '../../../error';
12+
import { path } from '../../../internal/utils/path';
1213

1314
export class Batches extends APIResource {
1415
/**
@@ -41,7 +42,7 @@ export class Batches extends APIResource {
4142
options?: RequestOptions,
4243
): APIPromise<BetaMessageBatch> {
4344
const { betas } = params ?? {};
44-
return this._client.get(`/v1/messages/batches/${messageBatchID}?beta=true`, {
45+
return this._client.get(path`/v1/messages/batches/${messageBatchID}?beta=true`, {
4546
...options,
4647
headers: buildHeaders([
4748
{ 'anthropic-beta': [...(betas ?? []), 'message-batches-2024-09-24'].toString() },
@@ -81,7 +82,7 @@ export class Batches extends APIResource {
8182
options?: RequestOptions,
8283
): APIPromise<BetaDeletedMessageBatch> {
8384
const { betas } = params ?? {};
84-
return this._client.delete(`/v1/messages/batches/${messageBatchID}?beta=true`, {
85+
return this._client.delete(path`/v1/messages/batches/${messageBatchID}?beta=true`, {
8586
...options,
8687
headers: buildHeaders([
8788
{ 'anthropic-beta': [...(betas ?? []), 'message-batches-2024-09-24'].toString() },
@@ -107,7 +108,7 @@ export class Batches extends APIResource {
107108
options?: RequestOptions,
108109
): APIPromise<BetaMessageBatch> {
109110
const { betas } = params ?? {};
110-
return this._client.post(`/v1/messages/batches/${messageBatchID}/cancel?beta=true`, {
111+
return this._client.post(path`/v1/messages/batches/${messageBatchID}/cancel?beta=true`, {
111112
...options,
112113
headers: buildHeaders([
113114
{ 'anthropic-beta': [...(betas ?? []), 'message-batches-2024-09-24'].toString() },

src/resources/beta/models.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APIResource } from '../../resource';
44
import { APIPromise } from '../../api-promise';
55
import { Page, type PageParams, PagePromise } from '../../pagination';
66
import { RequestOptions } from '../../internal/request-options';
7+
import { path } from '../../internal/utils/path';
78

89
export class Models extends APIResource {
910
/**
@@ -13,7 +14,7 @@ export class Models extends APIResource {
1314
* model or resolve a model alias to a model ID.
1415
*/
1516
retrieve(modelID: string, options?: RequestOptions): APIPromise<BetaModelInfo> {
16-
return this._client.get(`/v1/models/${modelID}?beta=true`, options);
17+
return this._client.get(path`/v1/models/${modelID}?beta=true`, options);
1718
}
1819

1920
/**

src/resources/messages/batches.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { buildHeaders } from '../../internal/headers';
99
import { RequestOptions } from '../../internal/request-options';
1010
import { JSONLDecoder } from '../../internal/decoders/jsonl';
1111
import { AnthropicError } from '../../error';
12+
import { path } from '../../internal/utils/path';
1213

1314
export class Batches extends APIResource {
1415
/**
@@ -28,7 +29,7 @@ export class Batches extends APIResource {
2829
* `results_url` field in the response.
2930
*/
3031
retrieve(messageBatchID: string, options?: RequestOptions): APIPromise<MessageBatch> {
31-
return this._client.get(`/v1/messages/batches/${messageBatchID}`, options);
32+
return this._client.get(path`/v1/messages/batches/${messageBatchID}`, options);
3233
}
3334

3435
/**
@@ -49,7 +50,7 @@ export class Batches extends APIResource {
4950
* like to delete an in-progress batch, you must first cancel it.
5051
*/
5152
delete(messageBatchID: string, options?: RequestOptions): APIPromise<DeletedMessageBatch> {
52-
return this._client.delete(`/v1/messages/batches/${messageBatchID}`, options);
53+
return this._client.delete(path`/v1/messages/batches/${messageBatchID}`, options);
5354
}
5455

5556
/**
@@ -64,7 +65,7 @@ export class Batches extends APIResource {
6465
* non-interruptible.
6566
*/
6667
cancel(messageBatchID: string, options?: RequestOptions): APIPromise<MessageBatch> {
67-
return this._client.post(`/v1/messages/batches/${messageBatchID}/cancel`, options);
68+
return this._client.post(path`/v1/messages/batches/${messageBatchID}/cancel`, options);
6869
}
6970

7071
/**
@@ -89,7 +90,6 @@ export class Batches extends APIResource {
8990
.get(batch.results_url, {
9091
...options,
9192
headers: buildHeaders([{ Accept: 'application/x-jsonl' }, options?.headers]),
92-
9393
__binaryResponse: true,
9494
})
9595
._thenUnwrap((_, props) => JSONLDecoder.fromResponse(props.response, props.controller));

src/resources/models.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APIResource } from '../resource';
44
import { APIPromise } from '../api-promise';
55
import { Page, type PageParams, PagePromise } from '../pagination';
66
import { RequestOptions } from '../internal/request-options';
7+
import { path } from '../internal/utils/path';
78

89
export class Models extends APIResource {
910
/**
@@ -13,7 +14,7 @@ export class Models extends APIResource {
1314
* model or resolve a model alias to a model ID.
1415
*/
1516
retrieve(modelID: string, options?: RequestOptions): APIPromise<ModelInfo> {
16-
return this._client.get(`/v1/models/${modelID}`, options);
17+
return this._client.get(path`/v1/models/${modelID}`, options);
1718
}
1819

1920
/**

0 commit comments

Comments
 (0)