Skip to content

Commit

Permalink
fix: exception thrown for large buffer -> base64 transforms (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Aug 27, 2023
1 parent 3c3f882 commit fc01172
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-mirrors-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cf-bindings-proxy': patch
---

Fix a maximum stack call exception from buffer -> base64 conversion
5 changes: 5 additions & 0 deletions .changeset/swift-weeks-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cf-bindings-proxy': patch
---

Fix blob -> base64 call for the arraybuffer not being awaited.
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"vite-plugin-externalize-deps": "^0.6.0",
"vitest": "^0.31.0",
"vitest-environment-miniflare": "^2.14.0",
"wrangler": "^3.5.1"
"wrangler": "^3.6.0"
},
"peerDependencies": {
"@cloudflare/workers-types": ">=4",
Expand Down
42 changes: 21 additions & 21 deletions src/cli/template/_worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BindingRequest, BindingResponse, PropertyCall } from '../../proxy';
import { transformData } from '../../transform';
import { prepareDataForProxy, transformData } from '../../transform';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Env = { [key: string]: any };
Expand All @@ -11,22 +11,26 @@ type Env = { [key: string]: any };
* @param callsToProcess Function calls to process.
* @returns The result of the function calls.
*/
const reduceCalls = (callee: Env, callsToProcess: PropertyCall[]): unknown => {
return callsToProcess.reduce((acc, { prop, args }) => {
return acc[prop](
...args.map((arg) => {
if (Array.isArray(arg.data)) {
return arg.data.map((a) => ('__bindingId' in a ? reduceCalls(callee, a.__calls) : a));
}
const reduceCalls = async (callee: Env, callsToProcess: PropertyCall[]): Promise<unknown> => {
return callsToProcess.reduce(async (acc, { prop, args }) => {
return (await acc)[prop](
...(await Promise.all(
args.map(async (arg) => {
if (Array.isArray(arg.data)) {
return Promise.all(
arg.data.map((a) => ('__bindingId' in a ? reduceCalls(callee, a.__calls) : a)),
);
}

if (arg.transform) {
return transformData(arg.data, arg.transform);
}
if (arg.transform) {
return transformData(arg.data, arg.transform);
}

return arg.data;
}),
return arg.data;
}),
)),
);
}, callee);
}, Promise.resolve(callee));
};

export default {
Expand All @@ -47,13 +51,9 @@ export default {
const resp: BindingResponse = { success: true, data: rawData };

if (resp.success) {
if (rawData instanceof ArrayBuffer) {
resp.transform = { from: 'base64', to: 'buffer' };
resp.data = transformData(rawData, { from: 'buffer', to: 'base64' });
} else if (rawData instanceof Blob) {
resp.transform = { from: 'base64', to: 'blob' };
resp.data = transformData(await rawData.arrayBuffer(), { from: 'buffer', to: 'base64' });
}
const transformedResp = await prepareDataForProxy(rawData, { data: rawData });
resp.transform = transformedResp.transform;
resp.data = transformedResp.data;
}

return new Response(JSON.stringify(resp), {
Expand Down
37 changes: 5 additions & 32 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
import { transformData } from './transform';
import { prepareDataForProxy, transformData } from './transform';

export type BindingResponse =
| { success: false; data: string; transform?: never }
| { success: true; data: unknown; transform?: { from: string; to: string } };

/**
* Prepares the property call argument to be sent to the proxy.
* This will transform any `ArrayBuffer` or `Blob` to `base64` and add the `transform` property.
*
* @param arg
*/
const preparePropertyCallArg = async (
arg: PropertyCall['args'][0],
): Promise<PropertyCall['args'][0]> => {
if (arg.data instanceof ArrayBuffer) {
return {
data: transformData(arg.data, { from: 'buffer', to: 'base64' }),
transform: { from: 'base64', to: 'buffer' },
};
}

if (arg.data instanceof Blob) {
return {
data: transformData(await arg.data.arrayBuffer(), {
from: 'buffer',
to: 'base64',
}),
transform: { from: 'base64', to: 'blob' },
};
}

return arg;
};

/**
* Prepares the binding request to be sent to the proxy.
*
Expand All @@ -44,7 +15,7 @@ const prepareBindingRequest = async (bindingRequest: BindingRequest): Promise<Bi
__calls: await Promise.all(
bindingRequest.__calls.map(async (call) => ({
...call,
args: await Promise.all(call.args.map(preparePropertyCallArg)),
args: await Promise.all(call.args.map((arg) => prepareDataForProxy(arg.data, arg))),
})),
),
};
Expand All @@ -57,10 +28,12 @@ const prepareBindingRequest = async (bindingRequest: BindingRequest): Promise<Bi
* @returns The data returned from the proxy.
*/
const fetchData = async (call: BindingRequest): Promise<unknown> => {
const preparedCall = await prepareBindingRequest(call);

let resp: Response;
try {
resp = await fetch('http://127.0.0.1:8799', {
body: JSON.stringify(await prepareBindingRequest(call)),
body: JSON.stringify(preparedCall),
method: 'POST',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
Expand Down
47 changes: 43 additions & 4 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
import type { PropertyCall } from './proxy';

/**
* Transforms data from one format to another.
*
* @param data Data to transform.
* @param transform Transform to apply.
* @returns Transformed data.
*/
export const transformData = (data: unknown, transform: { from: string; to: string }): unknown => {
export const transformData = async (
data: unknown,
transform: { from: string; to: string },
): Promise<unknown> => {
if (transform.from === 'buffer' && transform.to === 'base64') {
return btoa(String.fromCharCode(...new Uint8Array(data as ArrayBuffer)));
const bytes = new Uint8Array(data as ArrayBuffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i] as number);
}
return btoa(binary);
}

if (transform.from === 'base64' && transform.to === 'buffer') {
return Uint8Array.from(atob(data as string), (c) => c.charCodeAt(0)).buffer;
}

if (transform.from === 'blob' && transform.to === 'base64') {
const buffer = (data as Blob).arrayBuffer();
const buffer = await (data as Blob).arrayBuffer();
return transformData(buffer, { from: 'buffer', to: 'base64' });
}

if (transform.from === 'base64' && transform.to === 'blob') {
const buffer = transformData(data, { from: 'base64', to: 'buffer' }) as ArrayBuffer;
const buffer = (await transformData(data, { from: 'base64', to: 'buffer' })) as ArrayBuffer;
return new Blob([buffer]);
}

return data;
};

/**
* Prepares the argument's data to be sent over HTTP via the binding proxy.
* This will transform any `ArrayBuffer` or `Blob` to `base64` and add the `transform` property.
*
* @param data The data to prepare.
*/
export const prepareDataForProxy = async (
rawData: PropertyCallArg['data'],
fallback: PropertyCallArg,
): Promise<PropertyCallArg> => {
if (rawData instanceof ArrayBuffer) {
return {
transform: { from: 'base64', to: 'buffer' },
data: await transformData(rawData, { from: 'buffer', to: 'base64' }),
};
}

if (rawData instanceof Blob) {
return {
transform: { from: 'base64', to: 'blob' },
data: await transformData(rawData, { from: 'blob', to: 'base64' }),
};
}

return fallback;
};

type PropertyCallArg = PropertyCall['args'][0];

0 comments on commit fc01172

Please sign in to comment.