Skip to content

Commit

Permalink
feat!: Use base64 instead of hex encoding for binary data (#420)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Use the standard `encoding` field on the generated har file instead of `_isBinary` and use `base64` encoding instead of `hex` to reduce the payload size.
  • Loading branch information
offirgolan committed Nov 28, 2021
1 parent ede95ce commit 6bb9b36
Show file tree
Hide file tree
Showing 12 changed files with 53 additions and 51 deletions.
10 changes: 5 additions & 5 deletions packages/@pollyjs/adapter-fetch/src/index.js
Expand Up @@ -190,8 +190,8 @@ export default class FetchAdapter extends Adapter {
return {
statusCode: response.status,
headers: serializeHeaders(response.headers),
body: buffer.toString(isBinaryBuffer ? 'hex' : 'utf8'),
isBinary: isBinaryBuffer
body: buffer.toString(isBinaryBuffer ? 'base64' : 'utf8'),
encoding: isBinaryBuffer ? 'base64' : undefined
};
}

Expand Down Expand Up @@ -223,14 +223,14 @@ export default class FetchAdapter extends Adapter {
}

const { absoluteUrl, response: pollyResponse } = pollyRequest;
const { statusCode, body, isBinary } = pollyResponse;
const { statusCode, body, encoding } = pollyResponse;

let responseBody = body;

if (statusCode === 204 && responseBody === '') {
responseBody = null;
} else if (isBinary) {
responseBody = bufferToArrayBuffer(Buffer.from(body, 'hex'));
} else if (encoding) {
responseBody = bufferToArrayBuffer(Buffer.from(body, encoding));
}

const response = new Response(responseBody, {
Expand Down
32 changes: 16 additions & 16 deletions packages/@pollyjs/adapter-node-http/src/index.js
Expand Up @@ -220,13 +220,13 @@ export default class HttpAdapter extends Adapter {
headers: response.headers,
statusCode: response.statusCode,
body: responseBody.body,
isBinary: responseBody.isBinary
encoding: responseBody.encoding
};
}

async respondToRequest(pollyRequest, error) {
const { req, respond } = pollyRequest.requestArguments;
const { statusCode, body, headers, isBinary } = pollyRequest.response;
const { statusCode, body, headers, encoding } = pollyRequest.response;

if (pollyRequest[ABORT_HANDLER]) {
req.off('abort', pollyRequest[ABORT_HANDLER]);
Expand All @@ -248,7 +248,7 @@ export default class HttpAdapter extends Adapter {
return;
}

const chunks = this.getChunksFromBody(body, headers, isBinary);
const chunks = this.getChunksFromBody(body, headers, encoding);
const stream = new ReadableStream();

// Expose the response data as a stream of chunks since
Expand Down Expand Up @@ -281,7 +281,7 @@ export default class HttpAdapter extends Adapter {
// should not be concatenated. Instead, the chunks should
// be preserved as-is so that each chunk can be mocked individually
if (isContentEncoded(headers)) {
const hexChunks = chunks.map((chunk) => {
const encodedChunks = chunks.map((chunk) => {
if (!Buffer.isBuffer(chunk)) {
this.assert(
'content-encoded responses must all be binary buffers',
Expand All @@ -290,28 +290,28 @@ export default class HttpAdapter extends Adapter {
chunk = Buffer.from(chunk);
}

return chunk.toString('hex');
return chunk.toString('base64');
});

return {
isBinary: true,
body: JSON.stringify(hexChunks)
encoding: 'base64',
body: JSON.stringify(encodedChunks)
};
}

const buffer = mergeChunks(chunks);
const isBinaryBuffer = !isUtf8Representable(buffer);

// The merged buffer can be one of two things:
// 1. A binary buffer which then has to be recorded as a hex string.
// 1. A binary buffer which then has to be recorded as a base64 string.
// 2. A string buffer.
return {
isBinary: isBinaryBuffer,
body: buffer.toString(isBinaryBuffer ? 'hex' : 'utf8')
encoding: isBinaryBuffer ? 'base64' : undefined,
body: buffer.toString(isBinaryBuffer ? 'base64' : 'utf8')
};
}

getChunksFromBody(body, headers, isBinary = false) {
getChunksFromBody(body, headers, encoding) {
if (!body) {
return [];
}
Expand All @@ -321,16 +321,16 @@ export default class HttpAdapter extends Adapter {
}

// If content-encoding is set in the header then the body/content
// is as an array of hex strings
// is as an array of base64 strings
if (isContentEncoded(headers)) {
const hexChunks = JSON.parse(body);
const encodedChunks = JSON.parse(body);

return hexChunks.map((chunk) => Buffer.from(chunk, 'hex'));
return encodedChunks.map((chunk) => Buffer.from(chunk, encoding));
}

// The body can be one of two things:
// 1. A hex string which then means its binary data.
// 1. A base64 string which then means its binary data.
// 2. A utf8 string which means a regular string.
return [Buffer.from(body, isBinary ? 'hex' : 'utf8')];
return [Buffer.from(body, encoding ? encoding : 'utf8')];
}
}
Expand Up @@ -126,7 +126,7 @@ function commonTests(transport) {
expect(requests[0].body.toString('base64')).to.equal(
body.toString('base64')
);
expect(requests[0].identifiers.body).to.equal(body.toString('hex'));
expect(requests[0].identifiers.body).to.equal(body.toString('base64'));
});

it('should be able to upload form data', async function () {
Expand Down
12 changes: 6 additions & 6 deletions packages/@pollyjs/adapter-xhr/src/index.js
Expand Up @@ -132,14 +132,14 @@ export default class XHRAdapter extends Adapter {
const buffer = Buffer.from(arrayBuffer);

isBinary = !isBufferUtf8Representable(buffer);
body = buffer.toString(isBinary ? 'hex' : 'utf8');
body = buffer.toString(isBinary ? 'base64' : 'utf8');
}

return {
statusCode: xhr.status,
headers: serializeResponseHeaders(xhr.getAllResponseHeaders()),
body,
isBinary
encoding: isBinary ? 'base64' : undefined,
body
};
}

Expand All @@ -159,11 +159,11 @@ export default class XHRAdapter extends Adapter {
// https://github.com/sinonjs/nise/blob/v1.4.10/lib/fake-xhr/index.js#L614-L621
xhr.error();
} else {
const { statusCode, headers, body, isBinary } = pollyRequest.response;
const { statusCode, headers, body, encoding } = pollyRequest.response;
let responseBody = body;

if (isBinary) {
const buffer = Buffer.from(body, 'hex');
if (encoding) {
const buffer = Buffer.from(body, encoding);

if (BINARY_RESPONSE_TYPES.includes(xhr.responseType)) {
responseBody = bufferToArrayBuffer(buffer);
Expand Down
Expand Up @@ -8,7 +8,7 @@ export default function normalizeRecordedResponse(response) {
statusCode: status,
headers: normalizeHeaders(headers),
body: content && content.text,
isBinary: Boolean(content && content._isBinary)
encoding: content && content.encoding
};
}

Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/core/src/-private/request.js
Expand Up @@ -172,7 +172,7 @@ export default class PollyRequest extends HTTPBase {
}

async respond(response) {
const { statusCode, headers, body, isBinary = false } = response || {};
const { statusCode, headers, body, encoding } = response || {};

assert(
'Cannot respond to a request that already has a response.',
Expand All @@ -195,7 +195,7 @@ export default class PollyRequest extends HTTPBase {
// Set the body without modifying any headers (instead of using .send())
this.response.body = body;

this.response.isBinary = isBinary;
this.response.encoding = encoding;

// Trigger the `beforeResponse` event
await this._emit('beforeResponse', this.response);
Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/core/src/-private/response.js
Expand Up @@ -5,12 +5,12 @@ import HTTPBase from './http-base';
const DEFAULT_STATUS_CODE = 200;

export default class PollyResponse extends HTTPBase {
constructor(statusCode, headers, body, isBinary = false) {
constructor(statusCode, headers, body, encoding) {
super();
this.status(statusCode || DEFAULT_STATUS_CODE);
this.setHeaders(headers);
this.body = body;
this.isBinary = isBinary;
this.encoding = encoding;
}

get ok() {
Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/core/tests/unit/-private/response-test.js
Expand Up @@ -14,8 +14,8 @@ describe('Unit | Response', function () {
expect(new PollyResponse().statusCode).to.equal(200);
});

it('should default isBinary to false', function () {
expect(new PollyResponse().isBinary).to.be.false;
it('should default encoding to undefined', function () {
expect(new PollyResponse().encoding).to.be.undefined;
});

describe('API', function () {
Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/persister/src/har/response.js
Expand Up @@ -38,8 +38,8 @@ export default class Response {
if (response.body && typeof response.body === 'string') {
this.content.text = response.body;

if (response.isBinary) {
this.content._isBinary = true;
if (response.encoding) {
this.content.encoding = response.encoding;
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@pollyjs/utils/src/utils/serializers/buffer.js
Expand Up @@ -21,7 +21,7 @@ export function serialize(body) {
}

if (Buffer.isBuffer(buffer)) {
return buffer.toString('hex');
return buffer.toString('base64');
}
}

Expand Down
Expand Up @@ -18,36 +18,38 @@ describe('Unit | Utils | Serializers | buffer', function () {
it('should handle buffers', function () {
const buffer = Buffer.from('buffer');

expect(serialize(buffer)).to.equal(buffer.toString('hex'));
expect(serialize(buffer)).to.equal(buffer.toString('base64'));
});

it('should handle array of buffers', function () {
const buffers = [Buffer.from('b1'), Buffer.from('b2')];

expect(serialize(buffers)).to.include(buffers[0].toString('hex'));
expect(serialize(buffers)).to.include(buffers[1].toString('hex'));
expect(serialize(buffers)).to.include(buffers[0].toString('base64'));
expect(serialize(buffers)).to.include(buffers[1].toString('base64'));
});

it('should handle a mixed array of buffers and strings', function () {
const buffers = [Buffer.from('b1'), 's1'];

expect(serialize(buffers)).to.include(buffers[0].toString('hex'));
expect(serialize(buffers)).to.include(buffers[0].toString('base64'));
expect(serialize(buffers)).to.include(
Buffer.from(buffers[1]).toString('hex')
Buffer.from(buffers[1]).toString('base64')
);
});

it('should handle an ArrayBuffer', function () {
const buffer = new ArrayBuffer(8);

expect(serialize(buffer)).to.equal(Buffer.from(buffer).toString('hex'));
expect(serialize(buffer)).to.equal(Buffer.from(buffer).toString('base64'));
});

it('should handle an ArrayBufferView', function () {
const buffer = new Uint8Array(8);

expect(serialize(buffer)).to.equal(
Buffer.from(buffer, buffer.byteOffset, buffer.byteLength).toString('hex')
Buffer.from(buffer, buffer.byteOffset, buffer.byteLength).toString(
'base64'
)
);
});
});
12 changes: 6 additions & 6 deletions tests/integration/persister-tests.js
Expand Up @@ -365,12 +365,12 @@ export default function persisterTests() {
content = har.log.entries[0].response.content;

expect(await validate.har(har)).to.be.true;
expect(content._isBinary).to.be.undefined;
expect(content.encoding).to.be.undefined;

// Binary content
server.get(this.recordUrl()).once('beforeResponse', (req, res) => {
res.isBinary = true;
res.body = '536f6d6520636f6e74656e74';
res.encoding = 'base64';
res.body = 'U29tZSBjb250ZW50';
});

await this.fetchRecord();
Expand All @@ -380,11 +380,11 @@ export default function persisterTests() {
content = har.log.entries[1].response.content;

expect(await validate.har(har)).to.be.true;
expect(content._isBinary).to.be.true;
expect(content.encoding).to.equal('base64');

// Binary content with no body
server.get(this.recordUrl()).once('beforeResponse', (req, res) => {
res.isBinary = true;
res.encoding = 'base64';
res.body = '';
});

Expand All @@ -395,6 +395,6 @@ export default function persisterTests() {
content = har.log.entries[2].response.content;

expect(await validate.har(har)).to.be.true;
expect(content._isBinary).to.be.undefined;
expect(content.encoding).to.be.undefined;
});
}

0 comments on commit 6bb9b36

Please sign in to comment.