Skip to content

Commit 0e057f9

Browse files
stainless-botRobertCraigie
authored andcommitted
fix(client)!: uri encode path parameters
chore: unknown commit message
1 parent b157a4b commit 0e057f9

File tree

17 files changed

+440
-44
lines changed

17 files changed

+440
-44
lines changed

src/internal/utils/path.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { OpenAIError } 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 OpenAIError(`Path parameters result in path with invalid segments:\n${path}\n${underline}`);
55+
}
56+
57+
return path;
58+
};
59+
60+
/**
61+
* URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
62+
*/
63+
export const path = createPathTagFunction(encodeURIPath);

src/resources/batches.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as Shared from './shared';
66
import { APIPromise } from '../api-promise';
77
import { CursorPage, type CursorPageParams, PagePromise } from '../pagination';
88
import { RequestOptions } from '../internal/request-options';
9+
import { path } from '../internal/utils/path';
910

1011
export class Batches extends APIResource {
1112
/**
@@ -19,7 +20,7 @@ export class Batches extends APIResource {
1920
* Retrieves a batch.
2021
*/
2122
retrieve(batchID: string, options?: RequestOptions): APIPromise<Batch> {
22-
return this._client.get(`/batches/${batchID}`, options);
23+
return this._client.get(path`/batches/${batchID}`, options);
2324
}
2425

2526
/**
@@ -38,7 +39,7 @@ export class Batches extends APIResource {
3839
* (if any) available in the output file.
3940
*/
4041
cancel(batchID: string, options?: RequestOptions): APIPromise<Batch> {
41-
return this._client.post(`/batches/${batchID}/cancel`, options);
42+
return this._client.post(path`/batches/${batchID}/cancel`, options);
4243
}
4344
}
4445

src/resources/beta/assistants.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { APIPromise } from '../../api-promise';
1212
import { CursorPage, type CursorPageParams, PagePromise } from '../../pagination';
1313
import { buildHeaders } from '../../internal/headers';
1414
import { RequestOptions } from '../../internal/request-options';
15+
import { path } from '../../internal/utils/path';
1516

1617
export class Assistants extends APIResource {
1718
/**
@@ -29,7 +30,7 @@ export class Assistants extends APIResource {
2930
* Retrieves an assistant.
3031
*/
3132
retrieve(assistantID: string, options?: RequestOptions): APIPromise<Assistant> {
32-
return this._client.get(`/assistants/${assistantID}`, {
33+
return this._client.get(path`/assistants/${assistantID}`, {
3334
...options,
3435
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
3536
});
@@ -39,7 +40,7 @@ export class Assistants extends APIResource {
3940
* Modifies an assistant.
4041
*/
4142
update(assistantID: string, body: AssistantUpdateParams, options?: RequestOptions): APIPromise<Assistant> {
42-
return this._client.post(`/assistants/${assistantID}`, {
43+
return this._client.post(path`/assistants/${assistantID}`, {
4344
body,
4445
...options,
4546
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -64,7 +65,7 @@ export class Assistants extends APIResource {
6465
* Delete an assistant.
6566
*/
6667
delete(assistantID: string, options?: RequestOptions): APIPromise<AssistantDeleted> {
67-
return this._client.delete(`/assistants/${assistantID}`, {
68+
return this._client.delete(path`/assistants/${assistantID}`, {
6869
...options,
6970
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
7071
});

src/resources/beta/threads/messages.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { APIPromise } from '../../../api-promise';
77
import { CursorPage, type CursorPageParams, PagePromise } from '../../../pagination';
88
import { buildHeaders } from '../../../internal/headers';
99
import { RequestOptions } from '../../../internal/request-options';
10+
import { path } from '../../../internal/utils/path';
1011

1112
export class Messages extends APIResource {
1213
/**
1314
* Create a message.
1415
*/
1516
create(threadID: string, body: MessageCreateParams, options?: RequestOptions): APIPromise<Message> {
16-
return this._client.post(`/threads/${threadID}/messages`, {
17+
return this._client.post(path`/threads/${threadID}/messages`, {
1718
body,
1819
...options,
1920
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -25,7 +26,7 @@ export class Messages extends APIResource {
2526
*/
2627
retrieve(messageID: string, params: MessageRetrieveParams, options?: RequestOptions): APIPromise<Message> {
2728
const { thread_id } = params;
28-
return this._client.get(`/threads/${thread_id}/messages/${messageID}`, {
29+
return this._client.get(path`/threads/${thread_id}/messages/${messageID}`, {
2930
...options,
3031
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
3132
});
@@ -36,7 +37,7 @@ export class Messages extends APIResource {
3637
*/
3738
update(messageID: string, params: MessageUpdateParams, options?: RequestOptions): APIPromise<Message> {
3839
const { thread_id, ...body } = params;
39-
return this._client.post(`/threads/${thread_id}/messages/${messageID}`, {
40+
return this._client.post(path`/threads/${thread_id}/messages/${messageID}`, {
4041
body,
4142
...options,
4243
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -51,7 +52,7 @@ export class Messages extends APIResource {
5152
query: MessageListParams | null | undefined = {},
5253
options?: RequestOptions,
5354
): PagePromise<MessagesPage, Message> {
54-
return this._client.getAPIList(`/threads/${threadID}/messages`, CursorPage<Message>, {
55+
return this._client.getAPIList(path`/threads/${threadID}/messages`, CursorPage<Message>, {
5556
query,
5657
...options,
5758
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -67,7 +68,7 @@ export class Messages extends APIResource {
6768
options?: RequestOptions,
6869
): APIPromise<MessageDeleted> {
6970
const { thread_id } = params;
70-
return this._client.delete(`/threads/${thread_id}/messages/${messageID}`, {
71+
return this._client.delete(path`/threads/${thread_id}/messages/${messageID}`, {
7172
...options,
7273
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
7374
});

src/resources/beta/threads/runs/runs.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { RequestOptions } from '../../../../internal/request-options';
4040
import { AssistantStream, RunCreateParamsBaseStream } from '../../../../lib/AssistantStream';
4141
import { sleep } from '../../../../internal/utils/sleep';
4242
import { RunSubmitToolOutputsParamsStream } from '../../../../lib/AssistantStream';
43+
import { path } from '../../../../internal/utils/path';
4344

4445
export class Runs extends APIResource {
4546
steps: StepsAPI.Steps = new StepsAPI.Steps(this._client);
@@ -64,7 +65,7 @@ export class Runs extends APIResource {
6465
options?: RequestOptions,
6566
): APIPromise<Run> | APIPromise<Stream<AssistantsAPI.AssistantStreamEvent>> {
6667
const { include, ...body } = params;
67-
return this._client.post(`/threads/${threadID}/runs`, {
68+
return this._client.post(path`/threads/${threadID}/runs`, {
6869
query: { include },
6970
body,
7071
...options,
@@ -78,7 +79,7 @@ export class Runs extends APIResource {
7879
*/
7980
retrieve(runID: string, params: RunRetrieveParams, options?: RequestOptions): APIPromise<Run> {
8081
const { thread_id } = params;
81-
return this._client.get(`/threads/${thread_id}/runs/${runID}`, {
82+
return this._client.get(path`/threads/${thread_id}/runs/${runID}`, {
8283
...options,
8384
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
8485
});
@@ -89,7 +90,7 @@ export class Runs extends APIResource {
8990
*/
9091
update(runID: string, params: RunUpdateParams, options?: RequestOptions): APIPromise<Run> {
9192
const { thread_id, ...body } = params;
92-
return this._client.post(`/threads/${thread_id}/runs/${runID}`, {
93+
return this._client.post(path`/threads/${thread_id}/runs/${runID}`, {
9394
body,
9495
...options,
9596
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -104,7 +105,7 @@ export class Runs extends APIResource {
104105
query: RunListParams | null | undefined = {},
105106
options?: RequestOptions,
106107
): PagePromise<RunsPage, Run> {
107-
return this._client.getAPIList(`/threads/${threadID}/runs`, CursorPage<Run>, {
108+
return this._client.getAPIList(path`/threads/${threadID}/runs`, CursorPage<Run>, {
108109
query,
109110
...options,
110111
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -116,7 +117,7 @@ export class Runs extends APIResource {
116117
*/
117118
cancel(runID: string, params: RunCancelParams, options?: RequestOptions): APIPromise<Run> {
118119
const { thread_id } = params;
119-
return this._client.post(`/threads/${thread_id}/runs/${runID}/cancel`, {
120+
return this._client.post(path`/threads/${thread_id}/runs/${runID}/cancel`, {
120121
...options,
121122
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
122123
});
@@ -239,7 +240,7 @@ export class Runs extends APIResource {
239240
options?: RequestOptions,
240241
): APIPromise<Run> | APIPromise<Stream<AssistantsAPI.AssistantStreamEvent>> {
241242
const { thread_id, ...body } = params;
242-
return this._client.post(`/threads/${thread_id}/runs/${runID}/submit_tool_outputs`, {
243+
return this._client.post(path`/threads/${thread_id}/runs/${runID}/submit_tool_outputs`, {
243244
body,
244245
...options,
245246
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),

src/resources/beta/threads/runs/steps.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import { APIPromise } from '../../../../api-promise';
77
import { CursorPage, type CursorPageParams, PagePromise } from '../../../../pagination';
88
import { buildHeaders } from '../../../../internal/headers';
99
import { RequestOptions } from '../../../../internal/request-options';
10+
import { path } from '../../../../internal/utils/path';
1011

1112
export class Steps extends APIResource {
1213
/**
1314
* Retrieves a run step.
1415
*/
1516
retrieve(stepID: string, params: StepRetrieveParams, options?: RequestOptions): APIPromise<RunStep> {
1617
const { thread_id, run_id, ...query } = params;
17-
return this._client.get(`/threads/${thread_id}/runs/${run_id}/steps/${stepID}`, {
18+
return this._client.get(path`/threads/${thread_id}/runs/${run_id}/steps/${stepID}`, {
1819
query,
1920
...options,
2021
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -26,7 +27,7 @@ export class Steps extends APIResource {
2627
*/
2728
list(runID: string, params: StepListParams, options?: RequestOptions): PagePromise<RunStepsPage, RunStep> {
2829
const { thread_id, ...query } = params;
29-
return this._client.getAPIList(`/threads/${thread_id}/runs/${runID}/steps`, CursorPage<RunStep>, {
30+
return this._client.getAPIList(path`/threads/${thread_id}/runs/${runID}/steps`, CursorPage<RunStep>, {
3031
query,
3132
...options,
3233
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),

src/resources/beta/threads/threads.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { Stream } from '../../../streaming';
7272
import { buildHeaders } from '../../../internal/headers';
7373
import { RequestOptions } from '../../../internal/request-options';
7474
import { AssistantStream, ThreadCreateAndRunParamsBaseStream } from '../../../lib/AssistantStream';
75+
import { path } from '../../../internal/utils/path';
7576

7677
export class Threads extends APIResource {
7778
runs: RunsAPI.Runs = new RunsAPI.Runs(this._client);
@@ -92,7 +93,7 @@ export class Threads extends APIResource {
9293
* Retrieves a thread.
9394
*/
9495
retrieve(threadID: string, options?: RequestOptions): APIPromise<Thread> {
95-
return this._client.get(`/threads/${threadID}`, {
96+
return this._client.get(path`/threads/${threadID}`, {
9697
...options,
9798
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
9899
});
@@ -102,7 +103,7 @@ export class Threads extends APIResource {
102103
* Modifies a thread.
103104
*/
104105
update(threadID: string, body: ThreadUpdateParams, options?: RequestOptions): APIPromise<Thread> {
105-
return this._client.post(`/threads/${threadID}`, {
106+
return this._client.post(path`/threads/${threadID}`, {
106107
body,
107108
...options,
108109
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -113,7 +114,7 @@ export class Threads extends APIResource {
113114
* Delete a thread.
114115
*/
115116
delete(threadID: string, options?: RequestOptions): APIPromise<ThreadDeleted> {
116-
return this._client.delete(`/threads/${threadID}`, {
117+
return this._client.delete(path`/threads/${threadID}`, {
117118
...options,
118119
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
119120
});

src/resources/beta/vector-stores/file-batches.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RequestOptions } from '../../../internal/request-options';
1111
import { sleep } from '../../../internal/utils/sleep';
1212
import { type Uploadable } from '../../../uploads';
1313
import { allSettledWithThrow } from '../../../lib/Util';
14+
import { path } from '../../../internal/utils/path';
1415

1516
export class FileBatches extends APIResource {
1617
/**
@@ -21,7 +22,7 @@ export class FileBatches extends APIResource {
2122
body: FileBatchCreateParams,
2223
options?: RequestOptions,
2324
): APIPromise<VectorStoreFileBatch> {
24-
return this._client.post(`/vector_stores/${vectorStoreID}/file_batches`, {
25+
return this._client.post(path`/vector_stores/${vectorStoreID}/file_batches`, {
2526
body,
2627
...options,
2728
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -37,7 +38,7 @@ export class FileBatches extends APIResource {
3738
options?: RequestOptions,
3839
): APIPromise<VectorStoreFileBatch> {
3940
const { vector_store_id } = params;
40-
return this._client.get(`/vector_stores/${vector_store_id}/file_batches/${batchID}`, {
41+
return this._client.get(path`/vector_stores/${vector_store_id}/file_batches/${batchID}`, {
4142
...options,
4243
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
4344
});
@@ -53,7 +54,7 @@ export class FileBatches extends APIResource {
5354
options?: RequestOptions,
5455
): APIPromise<VectorStoreFileBatch> {
5556
const { vector_store_id } = params;
56-
return this._client.post(`/vector_stores/${vector_store_id}/file_batches/${batchID}/cancel`, {
57+
return this._client.post(path`/vector_stores/${vector_store_id}/file_batches/${batchID}/cancel`, {
5758
...options,
5859
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
5960
});
@@ -81,7 +82,7 @@ export class FileBatches extends APIResource {
8182
): PagePromise<VectorStoreFilesPage, FilesAPI.VectorStoreFile> {
8283
const { vector_store_id, ...query } = params;
8384
return this._client.getAPIList(
84-
`/vector_stores/${vector_store_id}/file_batches/${batchID}/files`,
85+
path`/vector_stores/${vector_store_id}/file_batches/${batchID}/files`,
8586
CursorPage<FilesAPI.VectorStoreFile>,
8687
{ query, ...options, headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]) },
8788
);

src/resources/beta/vector-stores/files.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { buildHeaders } from '../../../internal/headers';
88
import { RequestOptions } from '../../../internal/request-options';
99
import { sleep } from '../../../internal/utils';
1010
import { Uploadable } from '../../../uploads';
11+
import { path } from '../../../internal/utils/path';
1112

1213
export class Files extends APIResource {
1314
/**
@@ -20,7 +21,7 @@ export class Files extends APIResource {
2021
body: FileCreateParams,
2122
options?: RequestOptions,
2223
): APIPromise<VectorStoreFile> {
23-
return this._client.post(`/vector_stores/${vectorStoreID}/files`, {
24+
return this._client.post(path`/vector_stores/${vectorStoreID}/files`, {
2425
body,
2526
...options,
2627
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -36,7 +37,7 @@ export class Files extends APIResource {
3637
options?: RequestOptions,
3738
): APIPromise<VectorStoreFile> {
3839
const { vector_store_id } = params;
39-
return this._client.get(`/vector_stores/${vector_store_id}/files/${fileID}`, {
40+
return this._client.get(path`/vector_stores/${vector_store_id}/files/${fileID}`, {
4041
...options,
4142
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
4243
});
@@ -50,7 +51,7 @@ export class Files extends APIResource {
5051
query: FileListParams | null | undefined = {},
5152
options?: RequestOptions,
5253
): PagePromise<VectorStoreFilesPage, VectorStoreFile> {
53-
return this._client.getAPIList(`/vector_stores/${vectorStoreID}/files`, CursorPage<VectorStoreFile>, {
54+
return this._client.getAPIList(path`/vector_stores/${vectorStoreID}/files`, CursorPage<VectorStoreFile>, {
5455
query,
5556
...options,
5657
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
@@ -69,7 +70,7 @@ export class Files extends APIResource {
6970
options?: RequestOptions,
7071
): APIPromise<VectorStoreFileDeleted> {
7172
const { vector_store_id } = params;
72-
return this._client.delete(`/vector_stores/${vector_store_id}/files/${fileID}`, {
73+
return this._client.delete(path`/vector_stores/${vector_store_id}/files/${fileID}`, {
7374
...options,
7475
headers: buildHeaders([{ 'OpenAI-Beta': 'assistants=v2' }, options?.headers]),
7576
});

0 commit comments

Comments
 (0)