diff --git a/packages/core/src/http/requestBuilder.ts b/packages/core/src/http/requestBuilder.ts index 4cfa4ad7..d22421a4 100644 --- a/packages/core/src/http/requestBuilder.ts +++ b/packages/core/src/http/requestBuilder.ts @@ -499,16 +499,15 @@ export class DefaultRequestBuilder return { ...request, headers }; }); const result = await this.call(requestOptions); - if (result.body === '') { - throw new Error( - 'Could not parse body as JSON. The response body is empty.' - ); - } if (typeof result.body !== 'string') { throw new Error( 'Could not parse body as JSON. The response body is not a string.' ); } + if (result.body.trim() === '') { + // Try mapping the missing body as null + return this.tryMappingAsNull(schema, result); + } let parsed: unknown; try { parsed = JSON.parse(result.body); @@ -521,6 +520,20 @@ export class DefaultRequestBuilder } return { ...result, result: mappingResult.result }; } + + private tryMappingAsNull( + schema: Schema, + result: ApiResponse + ) { + const nullMappingResult = validateAndMap(null, schema); + if (nullMappingResult.errors) { + throw new Error( + 'Could not parse body as JSON. The response body is empty.' + ); + } + return { ...result, result: nullMappingResult.result }; + } + public async callAsXml( rootName: string, schema: Schema, diff --git a/packages/core/test/http/requestBuilder.test.ts b/packages/core/test/http/requestBuilder.test.ts index 3a8c71c1..2d3b0c4a 100644 --- a/packages/core/test/http/requestBuilder.test.ts +++ b/packages/core/test/http/requestBuilder.test.ts @@ -1,5 +1,6 @@ import { HttpClientInterface, + RequestBuilder, createRequestBuilderFactory, skipEncode, } from '../../src/http/requestBuilder'; @@ -24,6 +25,7 @@ import { FileWrapper } from '../../src/fileWrapper'; import fs from 'fs'; import path from 'path'; import { bossSchema } from '../../../schema/test/bossSchema'; +import { boolean, nullable, optional } from '@apimatic/schema/src'; describe('test default request builder behavior with succesful responses', () => { const authParams = { @@ -39,6 +41,16 @@ describe('test default request builder behavior with succesful responses', () => httpStatusCodesToRetry: [408, 413, 429, 500, 502, 503, 504, 521, 522, 524], httpMethodsToRetry: ['GET', 'PUT'] as HttpMethod[], }; + const noContentResponse: HttpResponse = { + statusCode: 204, + body: '', + headers: {}, + }; + const whitespacedResponse: HttpResponse = { + statusCode: 204, + body: ' ', + headers: {}, + }; const basicAuth = mockBasicAuthenticationInterface(authParams); const defaultRequestBuilder = createRequestBuilderFactory( mockHttpClientAdapter(), @@ -419,8 +431,7 @@ describe('test default request builder behavior with succesful responses', () => reqBuilder.validateResponse(false); await reqBuilder.callAsJson(employeeSchema); } catch (error) { - const expectedResult = - "Could not parse body as JSON.\n\nExpected 'r' instead of 'e'"; + const expectedResult = `Could not parse body as JSON.\n\nExpected 'r' instead of 'e'`; expect(error.message).toEqual(expectedResult); } }); @@ -473,6 +484,98 @@ describe('test default request builder behavior with succesful responses', () => expect(error.message).toEqual(`Response status code was not ok: 400.`); } }); + it('should test request builder with 400 response code', async () => { + try { + const reqBuilder = defaultRequestBuilder( + 'GET', + '/test/requestBuilder/errorResponse' + ); + reqBuilder.baseUrl('default'); + await reqBuilder.callAsText(); + } catch (error) { + expect(error.message).toEqual(`Response status code was not ok: 400.`); + } + }); + it('should test response with no content textual types', async () => { + const reqBuilder = customRequestBuilder(noContentResponse); + const { result } = await reqBuilder.callAsText(); + expect(result).toEqual(''); + }); + it('should test response with whitespace content textual types', async () => { + const reqBuilder = customRequestBuilder(whitespacedResponse); + const { result } = await reqBuilder.callAsText(); + expect(result).toEqual(' '); + }); + it('should test response with no content string cases', async () => { + const reqBuilder = customRequestBuilder(noContentResponse); + const nullableString = await reqBuilder.callAsJson(nullable(string())); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson(optional(string())); + expect(optionalString.result).toEqual(undefined); + }); + it('should test response with whitespace content string cases', async () => { + const reqBuilder = customRequestBuilder(whitespacedResponse); + const nullableString = await reqBuilder.callAsJson(nullable(string())); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson(optional(string())); + expect(optionalString.result).toEqual(undefined); + }); + it('should test response with no content boolean cases', async () => { + const reqBuilder = customRequestBuilder(noContentResponse); + const nullableString = await reqBuilder.callAsJson(nullable(boolean())); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson(optional(boolean())); + expect(optionalString.result).toEqual(undefined); + }); + it('should test response with whitespace content boolean cases', async () => { + const reqBuilder = customRequestBuilder(whitespacedResponse); + const nullableString = await reqBuilder.callAsJson(nullable(boolean())); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson(optional(boolean())); + expect(optionalString.result).toEqual(undefined); + }); + it('should test response with no content object cases', async () => { + const reqBuilder = customRequestBuilder(noContentResponse); + const nullableString = await reqBuilder.callAsJson( + nullable(employeeSchema) + ); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson( + optional(employeeSchema) + ); + expect(optionalString.result).toEqual(undefined); + }); + it('should test response with whitespace content object cases', async () => { + const reqBuilder = customRequestBuilder(whitespacedResponse); + const nullableString = await reqBuilder.callAsJson( + nullable(employeeSchema) + ); + expect(nullableString.result).toEqual(null); + + const optionalString = await reqBuilder.callAsJson( + optional(employeeSchema) + ); + expect(optionalString.result).toEqual(undefined); + }); + + function customRequestBuilder( + response: HttpResponse + ): RequestBuilder { + const reqBuilder = createRequestBuilderFactory( + mockHttpClientAdapter(response), + (server) => mockBaseURIProvider(server), + ApiError, + basicAuth, + retryConfig + )('GET', '/test/requestBuilder'); + reqBuilder.baseUrl('default'); + return reqBuilder; + } function mockBasicAuthenticationInterface({ username, @@ -497,16 +600,21 @@ describe('test default request builder behavior with succesful responses', () => }; } - function mockHttpClientAdapter(): HttpClientInterface { + function mockHttpClientAdapter( + customResponse?: HttpResponse + ): HttpClientInterface { return async (request, requestOptions) => { + if (typeof customResponse !== 'undefined') { + return customResponse; + } const iserrorResponse = request.url.startsWith( 'http://apimatic.hopto.org:3000/test/requestBuilder/errorResponse' ); if (iserrorResponse) { - return await mockErrorResponse(request, requestOptions); + return mockErrorResponse(request, requestOptions); } - return await mockResponse(request, requestOptions); + return mockResponse(request, requestOptions); }; }