Skip to content

Commit 0a65b4f

Browse files
fix(client)!: uri encode path parameters (#11)
1 parent 5a24b13 commit 0a65b4f

File tree

3 files changed

+385
-1
lines changed

3 files changed

+385
-1
lines changed

src/internal/utils/path.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { LightswitchError } 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 LightswitchError(
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/tasks.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APIResource } from '../resource';
44
import { APIPromise } from '../api-promise';
55
import { buildHeaders } from '../internal/headers';
66
import { RequestOptions } from '../internal/request-options';
7+
import { path } from '../internal/utils/path';
78

89
export class Tasks extends APIResource {
910
/**
@@ -21,7 +22,7 @@ export class Tasks extends APIResource {
2122
* Update a task
2223
*/
2324
update(taskID: number, body: TaskUpdateParams, options?: RequestOptions): APIPromise<Task> {
24-
return this._client.put(`/tasks/${taskID}`, { body, ...options });
25+
return this._client.put(path`/tasks/${taskID}`, { body, ...options });
2526
}
2627

2728
/**

0 commit comments

Comments
 (0)