Skip to content

Commit 8f03783

Browse files
authored
Allow Proxy to accept multiple content types (#278)
* Only process responses as JSON if the upstream call does not provide a specific `Accept` header * Transform all user-provided headers (i.e. the ones prefixed with `x-pd-proxy-`) to lowercase, to make the `Accept` header inspection easier * Merge the user-provided query params with the URL before the base64 encoding, so that users don't have to do it themselves * Bump package minor version
1 parent c1dc025 commit 8f03783

File tree

2 files changed

+106
-22
lines changed

2 files changed

+106
-22
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pipedream/sdk",
3-
"version": "2.2.1",
3+
"version": "2.3.0",
44
"private": false,
55
"repository": "github:PipedreamHQ/pipedream-sdk-typescript",
66
"type": "commonjs",

src/api/resources/proxy/client/Client.ts

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as core from "../../../../core/index.js";
66
import * as environments from "../../../../environments.js";
77
import * as errors from "../../../../errors/index.js";
88
import { base64Encode } from "../../../../core/index.js";
9+
import { createRequestUrl } from "../../../../core/fetcher/createRequestUrl.js";
910
import * as serializers from "../../../../serialization/index.js";
1011
import * as Pipedream from "../../../index.js";
1112

@@ -20,6 +21,19 @@ export declare namespace Proxy {
2021
export class Proxy {
2122
protected readonly _options: Proxy.Options;
2223

24+
/**
25+
* Precompiled regex to check if an Accept header accepts JSON responses.
26+
* Matches: application/json, */*, or application/* (with or without parameters)
27+
*/
28+
private static readonly JSON_ACCEPT_REGEX =
29+
/(?:^|,|\s)(application\/json|\*\/\*|application\/\*)(?:\s*;|\s*,|\s*$)/i;
30+
31+
/**
32+
* Precompiled regex to check if a Content-Type header is JSON.
33+
* Matches: application/json (with or without parameters like charset)
34+
*/
35+
private static readonly JSON_CONTENT_TYPE_REGEX = /^application\/json(?:\s|;|$)/i;
36+
2337
constructor(_options: Proxy.Options) {
2438
this._options = _options;
2539
}
@@ -34,11 +48,36 @@ export class Proxy {
3448

3549
const transformed: Record<string, string | core.Supplier<string | null | undefined> | null | undefined> = {};
3650
for (const [key, value] of Object.entries(headers)) {
37-
transformed[`x-pd-proxy-${key}`] = value;
51+
const headerName = `x-pd-proxy-${key.toLowerCase()}`;
52+
transformed[headerName] = value;
3853
}
3954
return transformed;
4055
}
4156

57+
/**
58+
* Determine the response type based on the `Accept` header provided by the
59+
* caller. Note that the caller's headers must have already been transformed
60+
* via `transformProxyHeaders`.
61+
*/
62+
private getResponseType(headers: core.Fetcher.Args["headers"]): core.Fetcher.Args["responseType"] | undefined {
63+
const acceptHeader = headers?.["x-pd-proxy-accept"];
64+
if (!acceptHeader) {
65+
return undefined; // No Accept header = default to JSON
66+
}
67+
68+
const acceptValue = typeof acceptHeader === 'string' ? acceptHeader : String(acceptHeader ?? '');
69+
const acceptsJson = Proxy.JSON_ACCEPT_REGEX.test(acceptValue);
70+
return acceptsJson ? undefined : "binary-response";
71+
}
72+
73+
/**
74+
* Check if the response is JSON based on content-type header
75+
*/
76+
private isJsonResponse(response: core.APIResponse<unknown, unknown>): boolean {
77+
const contentType = response.rawResponse.headers.get("content-type");
78+
return contentType ? Proxy.JSON_CONTENT_TYPE_REGEX.test(contentType) : false;
79+
}
80+
4281
/**
4382
* Forward an authenticated GET request to an external API using an external user's account credentials
4483
*
@@ -59,21 +98,21 @@ export class Proxy {
5998
public get(
6099
request: Pipedream.ProxyGetRequest,
61100
requestOptions?: Proxy.RequestOptions,
62-
): core.HttpResponsePromise<Pipedream.ProxyResponse | undefined> {
101+
): core.HttpResponsePromise<Pipedream.ProxyResponse | core.BinaryResponse | undefined> {
63102
return core.HttpResponsePromise.fromPromise(this.__get(request, requestOptions));
64103
}
65104

66105
private async __get(
67106
request: Pipedream.ProxyGetRequest,
68107
requestOptions?: Proxy.RequestOptions,
69-
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | undefined>> {
108+
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | core.BinaryResponse | undefined>> {
70109
const { url, externalUserId, accountId, params, headers } = request;
71-
const url64 = base64Encode(url);
110+
const urlWithParams = createRequestUrl(url, params);
111+
const url64 = base64Encode(urlWithParams);
72112
const transformedHeaders = this.transformProxyHeaders(headers);
73113
const _queryParams: Record<string, string | string[] | object | object[] | null> = {
74114
external_user_id: externalUserId,
75115
account_id: accountId,
76-
...(params || {}),
77116
};
78117
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
79118
this._options?.headers,
@@ -84,6 +123,7 @@ export class Proxy {
84123
transformedHeaders,
85124
requestOptions?.headers,
86125
);
126+
const responseType = this.getResponseType(_headers);
87127
const _response = await core.fetcher({
88128
url: core.url.join(
89129
(await core.Supplier.get(this._options.baseUrl)) ??
@@ -94,11 +134,19 @@ export class Proxy {
94134
method: "GET",
95135
headers: _headers,
96136
queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
137+
responseType,
97138
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
98139
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
99140
abortSignal: requestOptions?.abortSignal,
100141
});
101142
if (_response.ok) {
143+
if (!this.isJsonResponse(_response)) {
144+
return {
145+
data: _response.body as core.BinaryResponse,
146+
rawResponse: _response.rawResponse,
147+
};
148+
}
149+
102150
return {
103151
data: serializers.ProxyResponse.parseOrThrow(_response.body, {
104152
unrecognizedObjectKeys: "passthrough",
@@ -164,21 +212,21 @@ export class Proxy {
164212
public post(
165213
request: Pipedream.ProxyPostRequest,
166214
requestOptions?: Proxy.RequestOptions,
167-
): core.HttpResponsePromise<Pipedream.ProxyResponse | undefined> {
215+
): core.HttpResponsePromise<Pipedream.ProxyResponse | core.BinaryResponse | undefined> {
168216
return core.HttpResponsePromise.fromPromise(this.__post(request, requestOptions));
169217
}
170218

171219
private async __post(
172220
request: Pipedream.ProxyPostRequest,
173221
requestOptions?: Proxy.RequestOptions,
174-
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | undefined>> {
222+
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | core.BinaryResponse | undefined>> {
175223
const { url, externalUserId, accountId, body: _body, params, headers } = request;
176-
const url64 = base64Encode(url);
224+
const urlWithParams = createRequestUrl(url, params);
225+
const url64 = base64Encode(urlWithParams);
177226
const transformedHeaders = this.transformProxyHeaders(headers);
178227
const _queryParams: Record<string, string | string[] | object | object[] | null> = {
179228
external_user_id: externalUserId,
180229
account_id: accountId,
181-
...(params || {}),
182230
};
183231
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
184232
this._options?.headers,
@@ -189,6 +237,7 @@ export class Proxy {
189237
transformedHeaders,
190238
requestOptions?.headers,
191239
);
240+
const responseType = this.getResponseType(_headers);
192241
const _response = await core.fetcher({
193242
url: core.url.join(
194243
(await core.Supplier.get(this._options.baseUrl)) ??
@@ -205,11 +254,19 @@ export class Proxy {
205254
unrecognizedObjectKeys: "strip",
206255
omitUndefined: true,
207256
}),
257+
responseType,
208258
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
209259
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
210260
abortSignal: requestOptions?.abortSignal,
211261
});
212262
if (_response.ok) {
263+
if (!this.isJsonResponse(_response)) {
264+
return {
265+
data: _response.body as core.BinaryResponse,
266+
rawResponse: _response.rawResponse,
267+
};
268+
}
269+
213270
return {
214271
data: serializers.ProxyResponse.parseOrThrow(_response.body, {
215272
unrecognizedObjectKeys: "passthrough",
@@ -275,21 +332,21 @@ export class Proxy {
275332
public put(
276333
request: Pipedream.ProxyPutRequest,
277334
requestOptions?: Proxy.RequestOptions,
278-
): core.HttpResponsePromise<Pipedream.ProxyResponse | undefined> {
335+
): core.HttpResponsePromise<Pipedream.ProxyResponse | core.BinaryResponse | undefined> {
279336
return core.HttpResponsePromise.fromPromise(this.__put(request, requestOptions));
280337
}
281338

282339
private async __put(
283340
request: Pipedream.ProxyPutRequest,
284341
requestOptions?: Proxy.RequestOptions,
285-
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | undefined>> {
342+
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | core.BinaryResponse | undefined>> {
286343
const { url, externalUserId, accountId, body: _body, params, headers } = request;
287-
const url64 = base64Encode(url);
344+
const urlWithParams = createRequestUrl(url, params);
345+
const url64 = base64Encode(urlWithParams);
288346
const transformedHeaders = this.transformProxyHeaders(headers);
289347
const _queryParams: Record<string, string | string[] | object | object[] | null> = {
290348
external_user_id: externalUserId,
291349
account_id: accountId,
292-
...(params || {}),
293350
};
294351
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
295352
this._options?.headers,
@@ -300,6 +357,7 @@ export class Proxy {
300357
transformedHeaders,
301358
requestOptions?.headers,
302359
);
360+
const responseType = this.getResponseType(_headers);
303361
const _response = await core.fetcher({
304362
url: core.url.join(
305363
(await core.Supplier.get(this._options.baseUrl)) ??
@@ -316,11 +374,19 @@ export class Proxy {
316374
unrecognizedObjectKeys: "strip",
317375
omitUndefined: true,
318376
}),
377+
responseType,
319378
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
320379
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
321380
abortSignal: requestOptions?.abortSignal,
322381
});
323382
if (_response.ok) {
383+
if (!this.isJsonResponse(_response)) {
384+
return {
385+
data: _response.body as core.BinaryResponse,
386+
rawResponse: _response.rawResponse,
387+
};
388+
}
389+
324390
return {
325391
data: serializers.ProxyResponse.parseOrThrow(_response.body, {
326392
unrecognizedObjectKeys: "passthrough",
@@ -385,21 +451,21 @@ export class Proxy {
385451
public delete(
386452
request: Pipedream.ProxyDeleteRequest,
387453
requestOptions?: Proxy.RequestOptions,
388-
): core.HttpResponsePromise<Pipedream.ProxyResponse | undefined> {
454+
): core.HttpResponsePromise<Pipedream.ProxyResponse | core.BinaryResponse | undefined> {
389455
return core.HttpResponsePromise.fromPromise(this.__delete(request, requestOptions));
390456
}
391457

392458
private async __delete(
393459
request: Pipedream.ProxyDeleteRequest,
394460
requestOptions?: Proxy.RequestOptions,
395-
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | undefined>> {
461+
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | core.BinaryResponse | undefined>> {
396462
const { url, externalUserId, accountId, params, headers } = request;
397-
const url64 = base64Encode(url);
463+
const urlWithParams = createRequestUrl(url, params);
464+
const url64 = base64Encode(urlWithParams);
398465
const transformedHeaders = this.transformProxyHeaders(headers);
399466
const _queryParams: Record<string, string | string[] | object | object[] | null> = {
400467
external_user_id: externalUserId,
401468
account_id: accountId,
402-
...(params || {}),
403469
};
404470
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
405471
this._options?.headers,
@@ -410,6 +476,7 @@ export class Proxy {
410476
transformedHeaders,
411477
requestOptions?.headers,
412478
);
479+
const responseType = this.getResponseType(_headers);
413480
const _response = await core.fetcher({
414481
url: core.url.join(
415482
(await core.Supplier.get(this._options.baseUrl)) ??
@@ -420,11 +487,19 @@ export class Proxy {
420487
method: "DELETE",
421488
headers: _headers,
422489
queryParameters: { ..._queryParams, ...requestOptions?.queryParams },
490+
responseType,
423491
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
424492
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
425493
abortSignal: requestOptions?.abortSignal,
426494
});
427495
if (_response.ok) {
496+
if (!this.isJsonResponse(_response)) {
497+
return {
498+
data: _response.body as core.BinaryResponse,
499+
rawResponse: _response.rawResponse,
500+
};
501+
}
502+
428503
return {
429504
data: serializers.ProxyResponse.parseOrThrow(_response.body, {
430505
unrecognizedObjectKeys: "passthrough",
@@ -490,21 +565,21 @@ export class Proxy {
490565
public patch(
491566
request: Pipedream.ProxyPatchRequest,
492567
requestOptions?: Proxy.RequestOptions,
493-
): core.HttpResponsePromise<Pipedream.ProxyResponse | undefined> {
568+
): core.HttpResponsePromise<Pipedream.ProxyResponse | core.BinaryResponse | undefined> {
494569
return core.HttpResponsePromise.fromPromise(this.__patch(request, requestOptions));
495570
}
496571

497572
private async __patch(
498573
request: Pipedream.ProxyPatchRequest,
499574
requestOptions?: Proxy.RequestOptions,
500-
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | undefined>> {
575+
): Promise<core.WithRawResponse<Pipedream.ProxyResponse | core.BinaryResponse | undefined>> {
501576
const { url, externalUserId, accountId, body: _body, params, headers } = request;
502-
const url64 = base64Encode(url);
577+
const urlWithParams = createRequestUrl(url, params);
578+
const url64 = base64Encode(urlWithParams);
503579
const transformedHeaders = this.transformProxyHeaders(headers);
504580
const _queryParams: Record<string, string | string[] | object | object[] | null> = {
505581
external_user_id: externalUserId,
506582
account_id: accountId,
507-
...(params || {}),
508583
};
509584
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
510585
this._options?.headers,
@@ -515,6 +590,7 @@ export class Proxy {
515590
transformedHeaders,
516591
requestOptions?.headers,
517592
);
593+
const responseType = this.getResponseType(_headers);
518594
const _response = await core.fetcher({
519595
url: core.url.join(
520596
(await core.Supplier.get(this._options.baseUrl)) ??
@@ -531,11 +607,19 @@ export class Proxy {
531607
unrecognizedObjectKeys: "strip",
532608
omitUndefined: true,
533609
}),
610+
responseType,
534611
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
535612
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
536613
abortSignal: requestOptions?.abortSignal,
537614
});
538615
if (_response.ok) {
616+
if (!this.isJsonResponse(_response)) {
617+
return {
618+
data: _response.body as core.BinaryResponse,
619+
rawResponse: _response.rawResponse,
620+
};
621+
}
622+
539623
return {
540624
data: serializers.ProxyResponse.parseOrThrow(_response.body, {
541625
unrecognizedObjectKeys: "passthrough",

0 commit comments

Comments
 (0)