From 03694c86e547abe37c0b3a8db66968807ddf0c5d Mon Sep 17 00:00:00 2001 From: Bryn Ryans Date: Wed, 14 Jul 2021 16:59:21 -0700 Subject: [PATCH] fix: bullet proof APIX response handling # Description APIX now renders raw data if it cannot parse the response returned as expected. # Changes 1. Added try/catches in appropriate places. Note that the try/catch in RunIt is probably NOT necessary but the code being wrapped indicates that an error could be thrown (logically it should never happen as the user should always be authenticated). 2. Renamed `parseJson` method to `json2Csv` to better reflect what it does (I was initially confused by the original method name). 3. Created a ShowRaw response handler. 4. Created a fallbackResponseHandler to use ShowRaw. --- packages/run-it/src/RunIt.tsx | 18 ++++++-- .../components/DataGrid/gridUtils.spec.tsx | 6 +-- .../src/components/DataGrid/gridUtils.tsx | 2 +- .../run-it/src/components/DataGrid/index.ts | 2 +- .../ShowResponse/ShowResponse.spec.tsx | 12 ++++++ .../components/ShowResponse/ShowResponse.tsx | 13 ++++-- .../components/ShowResponse/responseUtils.tsx | 41 +++++++++++++------ packages/run-it/src/test-data/index.ts | 1 + packages/run-it/src/test-data/responses.ts | 10 +++++ 9 files changed, 82 insertions(+), 23 deletions(-) diff --git a/packages/run-it/src/RunIt.tsx b/packages/run-it/src/RunIt.tsx index 93556c6ca..0125c82b2 100644 --- a/packages/run-it/src/RunIt.tsx +++ b/packages/run-it/src/RunIt.tsx @@ -168,8 +168,9 @@ export const RunIt: FC = ({ tabs.onSelectTab(1) if (sdk) { setLoading(true) - setResponseContent( - await runRequest( + let response: ResponseContent + try { + response = await runRequest( sdk, basePath, httpMethod, @@ -178,7 +179,18 @@ export const RunIt: FC = ({ queryParams, body ) - ) + } catch (err) { + // This should not happen but it could. runRequest uses + // sdk.ok to login once. sdk.ok throws an error so fake + // out the response so that something can be rendered. + response = { + ok: false, + statusMessage: err.message ? err.message : 'Unknown error!', + statusCode: -1, + body: JSON.stringify(err), + } as ResponseContent + } + setResponseContent(response) } } diff --git a/packages/run-it/src/components/DataGrid/gridUtils.spec.tsx b/packages/run-it/src/components/DataGrid/gridUtils.spec.tsx index a7d6ed89a..af1c92082 100644 --- a/packages/run-it/src/components/DataGrid/gridUtils.spec.tsx +++ b/packages/run-it/src/components/DataGrid/gridUtils.spec.tsx @@ -24,7 +24,7 @@ */ -import { gridHeaders, gridRows, parseCsv, parseJson } from './gridUtils' +import { gridHeaders, gridRows, parseCsv, json2Csv } from './gridUtils' const dataRowsLength = 2 const allRowsLength = dataRowsLength + 1 @@ -59,7 +59,7 @@ describe('gridUtils', () => { }) test('parses json data', () => { - const actual = parseJson(testJsonData) + const actual = json2Csv(testJsonData) expect(actual).toBeDefined() expect(actual.data).toBeDefined() expect(actual.data).toHaveLength(allRowsLength) @@ -80,7 +80,7 @@ describe('gridUtils', () => { }) test('creates grid rows from json', () => { - const data = parseJson(testJsonData) + const data = json2Csv(testJsonData) const actual = gridRows(data.data) expect(actual).toHaveLength(allRowsLength) }) diff --git a/packages/run-it/src/components/DataGrid/gridUtils.tsx b/packages/run-it/src/components/DataGrid/gridUtils.tsx index 44e8447cc..983c4a115 100644 --- a/packages/run-it/src/components/DataGrid/gridUtils.tsx +++ b/packages/run-it/src/components/DataGrid/gridUtils.tsx @@ -37,7 +37,7 @@ export const parseCsv = (content: string) => { return Papa.parse(content.trim()) } -export const parseJson = (content: any) => { +export const json2Csv = (content: any) => { const csv = Papa.unparse(content) return parseCsv(csv) } diff --git a/packages/run-it/src/components/DataGrid/index.ts b/packages/run-it/src/components/DataGrid/index.ts index 5016ce5c8..4aee5a1ca 100644 --- a/packages/run-it/src/components/DataGrid/index.ts +++ b/packages/run-it/src/components/DataGrid/index.ts @@ -24,4 +24,4 @@ */ export { DataGrid } from './DataGrid' -export { parseCsv, parseJson } from './gridUtils' +export { parseCsv, json2Csv } from './gridUtils' diff --git a/packages/run-it/src/components/ShowResponse/ShowResponse.spec.tsx b/packages/run-it/src/components/ShowResponse/ShowResponse.spec.tsx index 41db4a743..4293f4fc8 100644 --- a/packages/run-it/src/components/ShowResponse/ShowResponse.spec.tsx +++ b/packages/run-it/src/components/ShowResponse/ShowResponse.spec.tsx @@ -35,6 +35,7 @@ import { testJsonResponse, testTextResponse, testUnknownResponse, + testBogusJsonResponse, } from '../../test-data' import { ShowResponse } from './ShowResponse' @@ -97,4 +98,15 @@ describe('ShowResponse', () => { screen.getByText(testErrorResponse.body.toString(), { exact: false }) ).toBeInTheDocument() }) + + test('it renders bogus json responses', () => { + renderWithTheme() + expect(screen.getByText('200: application/json')).toBeInTheDocument() + expect( + screen.getByText( + 'The response body could not be parsed. Displaying raw data.' + ) + ).toBeInTheDocument() + expect(screen.getByText('I AM A LYING JSON RESPONSE')).toBeInTheDocument() + }) }) diff --git a/packages/run-it/src/components/ShowResponse/ShowResponse.tsx b/packages/run-it/src/components/ShowResponse/ShowResponse.tsx index 5b8e86706..8e694e7a8 100644 --- a/packages/run-it/src/components/ShowResponse/ShowResponse.tsx +++ b/packages/run-it/src/components/ShowResponse/ShowResponse.tsx @@ -28,7 +28,7 @@ import React, { FC } from 'react' import { Heading } from '@looker/components' import { IRawResponse } from '@looker/sdk-rtl' -import { pickResponseHandler } from './responseUtils' +import { pickResponseHandler, fallbackResponseHandler } from './responseUtils' interface ShowResponseProps { /** A basic HTTP response for "raw" HTTP requests */ @@ -47,7 +47,14 @@ export const ShowResponse: FC = ({ verb, path, }) => { - const pickedHandler = pickResponseHandler(response) + // Bullet proof the rendered response. If for some reason we get a bad response or bad data in the + // response, render something + let renderedResponse + try { + renderedResponse = pickResponseHandler(response).component(response) + } catch (err) { + renderedResponse = fallbackResponseHandler().component(response) + } // TODO make a badge for the verb. // Once we are satisfied with the badge in the api-explorer package it should be moved here @@ -56,7 +63,7 @@ export const ShowResponse: FC = ({ {`${verb || ''} ${path || ''} ${response.statusCode}: ${ response.contentType }`} - {pickedHandler && pickedHandler.component(response)} + {renderedResponse} ) } diff --git a/packages/run-it/src/components/ShowResponse/responseUtils.tsx b/packages/run-it/src/components/ShowResponse/responseUtils.tsx index 69a668652..40d3bf9fb 100644 --- a/packages/run-it/src/components/ShowResponse/responseUtils.tsx +++ b/packages/run-it/src/components/ShowResponse/responseUtils.tsx @@ -25,10 +25,10 @@ */ import React, { ReactElement } from 'react' import { IRawResponse, ResponseMode, responseMode } from '@looker/sdk-rtl' -import { Paragraph, CodeBlock } from '@looker/components' +import { Paragraph, CodeBlock, MessageBar } from '@looker/components' import { CodeDisplay } from '@looker/code-editor' -import { DataGrid, parseCsv, parseJson } from '../DataGrid' +import { DataGrid, parseCsv, json2Csv } from '../DataGrid' /** * Are all items this array "simple" @@ -63,21 +63,17 @@ export const isColumnar = (data: any[]) => { * * Shows the JSON in a syntax-highlighted fashion * If the JSON is parseable as 2D row/column data it will also be shown in grid + * If JSON cannot be parsed it will be show as is * @param response */ const ShowJSON = (response: IRawResponse) => { const content = response.body.toString() - const data = parseJson(content) + const data = json2Csv(content) const showGrid = isColumnar(data.data) - const raw = ( - - ) - if (!showGrid) return raw - return + const json = JSON.stringify(JSON.parse(response.body), null, 2) + const raw = + if (showGrid) return + return raw } /** A handler for text type responses */ @@ -141,6 +137,21 @@ const ShowPDF = (response: IRawResponse) => { return ShowUnknown(response) } +/** A handler for responses that cannot be parsed */ +const ShowRaw = (response: IRawResponse) => ( + <> + {ShowUnknown(response)} + + The response body could not be parsed. Displaying raw data. + + + +) + interface Responder { /** A label indicating the supported MIME type(s) */ label: string @@ -213,3 +224,9 @@ export const pickResponseHandler = (response: IRawResponse) => { } return result } + +export const fallbackResponseHandler = (): Responder => ({ + label: 'unknown', + isRecognized: (contentType: string) => !!contentType, + component: (response) => ShowRaw(response), +}) diff --git a/packages/run-it/src/test-data/index.ts b/packages/run-it/src/test-data/index.ts index 759a45006..402b1ff05 100644 --- a/packages/run-it/src/test-data/index.ts +++ b/packages/run-it/src/test-data/index.ts @@ -31,5 +31,6 @@ export { testHtmlResponse, testUnknownResponse, testErrorResponse, + testBogusJsonResponse, } from './responses' export { api } from './specs' diff --git a/packages/run-it/src/test-data/responses.ts b/packages/run-it/src/test-data/responses.ts index 2977e0eca..52aafd536 100644 --- a/packages/run-it/src/test-data/responses.ts +++ b/packages/run-it/src/test-data/responses.ts @@ -106,3 +106,13 @@ export const testErrorResponse: IRawResponse = { statusCode: 404, statusMessage: 'some status message', } + +export const testBogusJsonResponse: IRawResponse = { + url: 'https://some/json/data', + headers: {}, + contentType: 'application/json', + ok: true, + statusCode: 200, + statusMessage: 'OK', + body: Buffer.from('I AM A LYING JSON RESPONSE'), +}