Improve request formatting
Summary: This diff adds several improvements to displaying requests / responses in the network plugin 1. If there is no formatter rendering the data, the data is rendered 'raw' 2. For binary data, `(binary data)` text is printed, rather than an empty box 3. For request that have an empty request / response, `(empty)` is printed, rather than an empty box 4. If a formatter is applied, we show which formatter was used, which makes it easier to debug / spot / report issues 5. If the graphql formatter can't parse the data because it is truncated, it won't return anything, so that it falls back to the next formatter (urlencoder) to take care of the presentation Changelog: [Network plugin] Improved presentation of request / response bodies and fixed issues where they would sometimes not be displayed. Reviewed By: jknoxville Differential Revision: D22865373 fbshipit-source-id: 912d2f27269a4c9edbf62f2039d46ccf90a008af
This commit is contained in:
committed by
Facebook GitHub Bot
parent
45d461a3b8
commit
b7cfb509f1
@@ -19,6 +19,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
styled,
|
styled,
|
||||||
colors,
|
colors,
|
||||||
|
SmallText,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {decodeBody, getHeaderValue} from './utils';
|
import {decodeBody, getHeaderValue} from './utils';
|
||||||
import {formatBytes} from './index';
|
import {formatBytes} from './index';
|
||||||
@@ -332,14 +333,23 @@ class RequestBodyInspector extends Component<{
|
|||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {request, formattedText} = this.props;
|
const {request, formattedText} = this.props;
|
||||||
|
if (request.data == null || request.data.trim() === '') {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
||||||
let component;
|
|
||||||
for (const formatter of bodyFormatters) {
|
for (const formatter of bodyFormatters) {
|
||||||
if (formatter.formatRequest) {
|
if (formatter.formatRequest) {
|
||||||
try {
|
try {
|
||||||
component = formatter.formatRequest(request);
|
const component = formatter.formatRequest(request);
|
||||||
if (component) {
|
if (component) {
|
||||||
break;
|
return (
|
||||||
|
<BodyContainer>
|
||||||
|
{component}
|
||||||
|
<FormattedBy>
|
||||||
|
Formatted by {formatter.constructor.name}
|
||||||
|
</FormattedBy>
|
||||||
|
</BodyContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -349,10 +359,7 @@ class RequestBodyInspector extends Component<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return renderRawBody(request);
|
||||||
component = component || <Text>{decodeBody(request)}</Text>;
|
|
||||||
|
|
||||||
return <BodyContainer>{component}</BodyContainer>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,14 +370,23 @@ class ResponseBodyInspector extends Component<{
|
|||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {request, response, formattedText} = this.props;
|
const {request, response, formattedText} = this.props;
|
||||||
|
if (response.data == null || response.data.trim() === '') {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
||||||
let component;
|
|
||||||
for (const formatter of bodyFormatters) {
|
for (const formatter of bodyFormatters) {
|
||||||
if (formatter.formatResponse) {
|
if (formatter.formatResponse) {
|
||||||
try {
|
try {
|
||||||
component = formatter.formatResponse(request, response);
|
const component = formatter.formatResponse(request, response);
|
||||||
if (component) {
|
if (component) {
|
||||||
break;
|
return (
|
||||||
|
<BodyContainer>
|
||||||
|
{component}
|
||||||
|
<FormattedBy>
|
||||||
|
Formatted by {formatter.constructor.name}
|
||||||
|
</FormattedBy>
|
||||||
|
</BodyContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -380,13 +396,43 @@ class ResponseBodyInspector extends Component<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return renderRawBody(response);
|
||||||
component = component || <Text>{decodeBody(response)}</Text>;
|
|
||||||
|
|
||||||
return <BodyContainer>{component}</BodyContainer>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FormattedBy = styled(SmallText)({
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: '0.7em',
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'block',
|
||||||
|
});
|
||||||
|
|
||||||
|
const Empty = () => (
|
||||||
|
<BodyContainer>
|
||||||
|
<Text>(empty)</Text>
|
||||||
|
</BodyContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderRawBody(container: Request | Response) {
|
||||||
|
const decoded = decodeBody(container);
|
||||||
|
return (
|
||||||
|
<BodyContainer>
|
||||||
|
{decoded ? (
|
||||||
|
<Text selectable wordWrap="break-word">
|
||||||
|
{decoded}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FormattedBy>(Failed to decode)</FormattedBy>
|
||||||
|
<Text selectable wordWrap="break-word">
|
||||||
|
{container.data}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</BodyContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const MediaContainer = styled(FlexColumn)({
|
const MediaContainer = styled(FlexColumn)({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -662,7 +708,11 @@ class GraphQLFormatter {
|
|||||||
};
|
};
|
||||||
formatRequest = (request: Request) => {
|
formatRequest = (request: Request) => {
|
||||||
if (request.url.indexOf('graphql') > 0) {
|
if (request.url.indexOf('graphql') > 0) {
|
||||||
const data = querystring.parse(decodeBody(request));
|
const decoded = decodeBody(request);
|
||||||
|
if (!decoded) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const data = querystring.parse(decoded);
|
||||||
if (typeof data.variables === 'string') {
|
if (typeof data.variables === 'string') {
|
||||||
data.variables = JSON.parse(data.variables);
|
data.variables = JSON.parse(data.variables);
|
||||||
}
|
}
|
||||||
@@ -725,16 +775,40 @@ class FormUrlencodedFormatter {
|
|||||||
formatRequest = (request: Request) => {
|
formatRequest = (request: Request) => {
|
||||||
const contentType = getHeaderValue(request.headers, 'content-type');
|
const contentType = getHeaderValue(request.headers, 'content-type');
|
||||||
if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
||||||
|
const decoded = decodeBody(request);
|
||||||
|
if (!decoded) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ManagedDataInspector
|
<ManagedDataInspector
|
||||||
expandRoot={true}
|
expandRoot={true}
|
||||||
data={querystring.parse(decodeBody(request))}
|
data={querystring.parse(decoded)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BinaryFormatter {
|
||||||
|
formatRequest(request: Request) {
|
||||||
|
return this.format(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatResponse(_request: Request, response: Response) {
|
||||||
|
return this.format(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
format(container: Request | Response) {
|
||||||
|
if (
|
||||||
|
getHeaderValue(container.headers, 'content-type') ===
|
||||||
|
'application/octet-stream'
|
||||||
|
) {
|
||||||
|
return '(binary data)'; // we could offer a download button here?
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const BodyFormatters: Array<BodyFormatter> = [
|
const BodyFormatters: Array<BodyFormatter> = [
|
||||||
new ImageFormatter(),
|
new ImageFormatter(),
|
||||||
new VideoFormatter(),
|
new VideoFormatter(),
|
||||||
@@ -744,6 +818,7 @@ const BodyFormatters: Array<BodyFormatter> = [
|
|||||||
new JSONFormatter(),
|
new JSONFormatter(),
|
||||||
new FormUrlencodedFormatter(),
|
new FormUrlencodedFormatter(),
|
||||||
new XMLTextFormatter(),
|
new XMLTextFormatter(),
|
||||||
|
new BinaryFormatter(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const TextBodyFormatters: Array<BodyFormatter> = [new JSONTextFormatter()];
|
const TextBodyFormatters: Array<BodyFormatter> = [new JSONTextFormatter()];
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function decodeBody(container: Request | Response): string {
|
|||||||
|
|
||||||
return b64Decoded;
|
return b64Decoded;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Discarding malformed body, size: ' + b64Decoded.length);
|
console.warn(
|
||||||
|
`Flipper failed to decode request/response body (size: ${b64Decoded.length}): ${e}`,
|
||||||
|
);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,9 @@ function decompress(body: string): string {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Sometimes Content-Encoding is 'gzip' but the body is already decompressed.
|
// Sometimes Content-Encoding is 'gzip' but the body is already decompressed.
|
||||||
// Assume this is the case when decompression fails.
|
// Assume this is the case when decompression fails.
|
||||||
|
if (!('' + e).includes('incorrect header check')) {
|
||||||
|
console.warn('decompression failed: ' + e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
|
|||||||
Reference in New Issue
Block a user