diff --git a/tensorboard/webapp/webapp_data_source/tb_http_client.ts b/tensorboard/webapp/webapp_data_source/tb_http_client.ts index c0969f84a9..2acc98f82f 100644 --- a/tensorboard/webapp/webapp_data_source/tb_http_client.ts +++ b/tensorboard/webapp/webapp_data_source/tb_http_client.ts @@ -45,6 +45,20 @@ function convertFormDataToObject(formData: FormData) { return result; } +function bodyToParams(body: any | null, serializeUnder?: string) { + if (!body) { + return; + } + const params = + body instanceof FormData ? convertFormDataToObject(body) : body; + if (serializeUnder) { + return { + [serializeUnder]: JSON.stringify(params), + }; + } + return params; +} + export const XSRF_REQUIRED_HEADER = 'X-XSRF-Protected'; /** @@ -84,7 +98,8 @@ export class TBHttpClient implements TBHttpClientInterface { path: string, // Angular's HttpClient is typed exactly this way. body: any | null, - options: PostOptions | undefined = {} + options: PostOptions | undefined = {}, + serializeUnder: string | undefined = undefined ): Observable { options = withXsrfHeader(options); return this.store.select(getIsFeatureFlagsLoaded).pipe( @@ -100,7 +115,7 @@ export class TBHttpClient implements TBHttpClientInterface { if (isInColab) { return this.http.get(resolvedPath, { headers: options.headers ?? {}, - params: convertFormDataToObject(body), + params: bodyToParams(body, serializeUnder), }); } else { return this.http.post(resolvedPath, body, options); diff --git a/tensorboard/webapp/webapp_data_source/tb_http_client_test.ts b/tensorboard/webapp/webapp_data_source/tb_http_client_test.ts index 0be777ecf2..5353e7cdbc 100644 --- a/tensorboard/webapp/webapp_data_source/tb_http_client_test.ts +++ b/tensorboard/webapp/webapp_data_source/tb_http_client_test.ts @@ -92,18 +92,51 @@ describe('TBHttpClient', () => { }); }); - it('converts POST requests to GET when in Colab', () => { - const body = new FormData(); - body.append('formKey', 'value'); - store.overrideSelector(getIsFeatureFlagsLoaded, true); - store.overrideSelector(getIsInColab, true); - tbHttpClient.post('foo', body).subscribe(jasmine.createSpy()); - httpMock.expectOne((req) => { - return ( - req.method === 'GET' && - req.urlWithParams === 'foo?formKey=value' && - !req.body - ); + describe('converts POST requests to GET when in Colab', () => { + it('using form data', () => { + const body = new FormData(); + body.append('formKey', 'value'); + store.overrideSelector(getIsFeatureFlagsLoaded, true); + store.overrideSelector(getIsInColab, true); + tbHttpClient.post('foo', body).subscribe(jasmine.createSpy()); + httpMock.expectOne((req) => { + return ( + req.method === 'GET' && + req.urlWithParams === 'foo?formKey=value' && + !req.body + ); + }); + }); + + it('using json', () => { + const body = {key: 'value'}; + store.overrideSelector(getIsFeatureFlagsLoaded, true); + store.overrideSelector(getIsInColab, true); + tbHttpClient.post('foo', body).subscribe(jasmine.createSpy()); + httpMock.expectOne((req) => { + return ( + req.method === 'GET' && + req.urlWithParams === 'foo?key=value' && + !req.body + ); + }); + }); + + it('sets body as a serialized query param when serializeUnder is set', () => { + const body = {key: 'value', foo: [1, 2, 3]}; + store.overrideSelector(getIsFeatureFlagsLoaded, true); + store.overrideSelector(getIsInColab, true); + tbHttpClient + .post('foo', body, {}, 'request') + .subscribe(jasmine.createSpy()); + httpMock.expectOne((req) => { + return ( + req.method === 'GET' && + req.urlWithParams === + 'foo?request=%7B%22key%22:%22value%22,%22foo%22:%5B1,2,3%5D%7D' && + !req.body + ); + }); }); });