Guess content type if no header present
Summary: Changelog: [Network] The network plugin will now detect utf-8 strings if no content header is present Fixes https://github.com/facebook/flipper/issues/2406 Reviewed By: nikoant Differential Revision: D29388968 fbshipit-source-id: 7017828a5f3f28dcf220eeda1d30888f1fc5f07a
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2b0ce88c22
commit
9f27b374f4
@@ -82,8 +82,8 @@ const StyledCollapse = styled(Collapse)({
|
|||||||
background: theme.backgroundWash,
|
background: theme.backgroundWash,
|
||||||
paddingTop: theme.space.tiny,
|
paddingTop: theme.space.tiny,
|
||||||
paddingBottom: theme.space.tiny,
|
paddingBottom: theme.space.tiny,
|
||||||
paddingLeft: 26,
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
'> .anticon': {
|
'> .anticon': {
|
||||||
padding: `5px 0px`,
|
padding: `5px 0px`,
|
||||||
left: 8,
|
left: 8,
|
||||||
|
|||||||
@@ -23,13 +23,7 @@ import {
|
|||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {Select, Typography} from 'antd';
|
import {Select, Typography} from 'antd';
|
||||||
|
|
||||||
import {
|
import {bodyAsBinary, bodyAsString, formatBytes, getHeaderValue} from './utils';
|
||||||
bodyAsBinary,
|
|
||||||
bodyAsString,
|
|
||||||
formatBytes,
|
|
||||||
getHeaderValue,
|
|
||||||
isTextual,
|
|
||||||
} from './utils';
|
|
||||||
import {Request, Header, Insights, RetryInsights} from './types';
|
import {Request, Header, Insights, RetryInsights} from './types';
|
||||||
import {BodyOptions} from './index';
|
import {BodyOptions} from './index';
|
||||||
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
|
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
|
||||||
@@ -95,7 +89,7 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
key="requestData"
|
key="requestData"
|
||||||
title={'Request Body'}
|
title={'Request Body'}
|
||||||
extraActions={
|
extraActions={
|
||||||
isTextual(request.requestHeaders) ? (
|
typeof request.requestData === 'string' ? (
|
||||||
<CopyOutlined
|
<CopyOutlined
|
||||||
title="Copy request body"
|
title="Copy request body"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -129,7 +123,8 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
request.responseIsMock ? ' (Mocked)' : ''
|
request.responseIsMock ? ' (Mocked)' : ''
|
||||||
}`}
|
}`}
|
||||||
extraActions={
|
extraActions={
|
||||||
isTextual(request.responseHeaders) && request.responseData ? (
|
typeof request.responseData === 'string' &&
|
||||||
|
request.responseData ? (
|
||||||
<CopyOutlined
|
<CopyOutlined
|
||||||
title="Copy response body"
|
title="Copy response body"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {readFile} from 'fs';
|
import {readFile} from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {decodeBody} from '../utils';
|
import {decodeBody, isTextual} from '../utils';
|
||||||
import {ResponseInfo} from '../types';
|
import {ResponseInfo} from '../types';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
@@ -139,6 +139,19 @@ describe('network data encoding', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('detects utf8 strings in binary arrays', async () => {
|
||||||
|
const binaryBuffer = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'tiny_logo.png'),
|
||||||
|
);
|
||||||
|
const textBuffer = readFileSync(
|
||||||
|
path.join(__dirname, 'fixtures', 'donating.md'),
|
||||||
|
);
|
||||||
|
const textBuffer2 = readFileSync(__filename);
|
||||||
|
expect(isTextual(undefined, binaryBuffer)).toBe(false);
|
||||||
|
expect(isTextual(undefined, textBuffer)).toBe(true);
|
||||||
|
expect(isTextual(undefined, textBuffer2)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('binary data gets serialized correctly', async () => {
|
test('binary data gets serialized correctly', async () => {
|
||||||
const tinyLogoExpected = readFileSync(
|
const tinyLogoExpected = readFileSync(
|
||||||
path.join(__dirname, 'fixtures', 'tiny_logo.png'),
|
path.join(__dirname, 'fixtures', 'tiny_logo.png'),
|
||||||
|
|||||||
@@ -28,23 +28,66 @@ export function getHeaderValue(
|
|||||||
|
|
||||||
// Matches `application/json` and `application/vnd.api.v42+json` (see https://jsonapi.org/#mime-types)
|
// Matches `application/json` and `application/vnd.api.v42+json` (see https://jsonapi.org/#mime-types)
|
||||||
const jsonContentTypeRegex = new RegExp('application/(json|.+\\+json)');
|
const jsonContentTypeRegex = new RegExp('application/(json|.+\\+json)');
|
||||||
|
const binaryContentType =
|
||||||
|
/^(application\/(zip|octet-stream|pdf))|(video|audio)|(image\/(png|webp|jpeg|gif|avif))$/;
|
||||||
|
|
||||||
export function isTextual(headers?: Array<Header>): boolean {
|
export function isTextual(
|
||||||
|
headers?: Array<Header>,
|
||||||
|
body?: Uint8Array | string,
|
||||||
|
): boolean {
|
||||||
const contentType = getHeaderValue(headers, 'Content-Type');
|
const contentType = getHeaderValue(headers, 'Content-Type');
|
||||||
if (!contentType) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||||
return (
|
if (contentType) {
|
||||||
contentType.startsWith('text/') ||
|
if (
|
||||||
contentType.startsWith('application/x-www-form-urlencoded') ||
|
contentType.startsWith('text/') ||
|
||||||
jsonContentTypeRegex.test(contentType) ||
|
contentType.startsWith('application/x-www-form-urlencoded') ||
|
||||||
contentType.startsWith('multipart/') ||
|
jsonContentTypeRegex.test(contentType) ||
|
||||||
contentType.startsWith('message/') ||
|
contentType.startsWith('multipart/') ||
|
||||||
contentType.startsWith('image/svg') ||
|
contentType.startsWith('message/') ||
|
||||||
contentType.startsWith('application/xhtml+xml')
|
contentType.startsWith('image/svg') ||
|
||||||
);
|
contentType.startsWith('application/xhtml+xml') ||
|
||||||
|
contentType.startsWith('application/xml')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (binaryContentType.test(contentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(body instanceof Buffer || body instanceof Uint8Array) &&
|
||||||
|
isValidUtf8(body)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUtf8(data: Uint8Array) {
|
||||||
|
if (data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) {
|
||||||
|
return true; // valid utf8 BOM
|
||||||
|
}
|
||||||
|
// From https://weblog.rogueamoeba.com/2017/02/27/javascript-correctly-converting-a-byte-array-to-a-utf-8-string/
|
||||||
|
const extraByteMap = [1, 1, 1, 1, 2, 2, 3, 0];
|
||||||
|
const count = data.length;
|
||||||
|
|
||||||
|
for (let index = 0; index < count; ) {
|
||||||
|
let ch = data[index++];
|
||||||
|
if (ch & 0x80) {
|
||||||
|
let extra = extraByteMap[(ch >> 3) & 0x07];
|
||||||
|
if (!(ch & 0x40) || !extra || index + extra > count) return false;
|
||||||
|
|
||||||
|
ch &= 0x3f >> extra;
|
||||||
|
for (; extra > 0; extra -= 1) {
|
||||||
|
const chx = data[index++];
|
||||||
|
if ((chx & 0xc0) != 0x80) return false;
|
||||||
|
|
||||||
|
ch = (ch << 6) | (chx & 0x3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeBody(
|
export function decodeBody(
|
||||||
@@ -62,7 +105,7 @@ export function decodeBody(
|
|||||||
// The request is gzipped, so convert the raw bytes back to base64 first.
|
// The request is gzipped, so convert the raw bytes back to base64 first.
|
||||||
const dataArr = Base64.toUint8Array(data);
|
const dataArr = Base64.toUint8Array(data);
|
||||||
// then inflate.
|
// then inflate.
|
||||||
return isTextual(headers)
|
return isTextual(headers, dataArr)
|
||||||
? // pako will detect the BOM headers and return a proper utf-8 string right away
|
? // pako will detect the BOM headers and return a proper utf-8 string right away
|
||||||
pako.inflate(dataArr, {to: 'string'})
|
pako.inflate(dataArr, {to: 'string'})
|
||||||
: pako.inflate(dataArr);
|
: pako.inflate(dataArr);
|
||||||
@@ -78,10 +121,11 @@ export function decodeBody(
|
|||||||
// If this is not a gzipped request, assume we are interested in a proper utf-8 string.
|
// If this is not a gzipped request, assume we are interested in a proper utf-8 string.
|
||||||
// - If the raw binary data in is needed, in base64 form, use data directly
|
// - If the raw binary data in is needed, in base64 form, use data directly
|
||||||
// - either directly use data (for example)
|
// - either directly use data (for example)
|
||||||
if (isTextual(headers)) {
|
const bytes = Base64.toUint8Array(data);
|
||||||
|
if (isTextual(headers, bytes)) {
|
||||||
return Base64.decode(data);
|
return Base64.decode(data);
|
||||||
} else {
|
} else {
|
||||||
return Base64.toUint8Array(data);
|
return bytes;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|||||||
Reference in New Issue
Block a user