Skip to content

Commit

Permalink
fix: handle application/x-www-form-urlencoded content in request body
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Jul 17, 2024
1 parent 9ba3266 commit f6be838
Show file tree
Hide file tree
Showing 49 changed files with 584 additions and 159 deletions.
6 changes: 6 additions & 0 deletions .changeset/smooth-dingos-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hey-api/client-fetch': patch
'@hey-api/openapi-ts': patch
---

fix: handle application/x-www-form-urlencoded content in request body
2 changes: 1 addition & 1 deletion packages/client-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type ApiRequestOptions<T = unknown> = {
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown>;
readonly formData?: Record<string, unknown> | Record<string, unknown>[];
readonly headers?: Record<string, unknown>;
readonly mediaType?: string;
readonly method:
Expand Down
4 changes: 0 additions & 4 deletions packages/client-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ export const createClient = (config: Config): Client => {
redirect: 'follow',
...opts,
};
// remove Content-Type if serialized body is FormData; browser will correctly set Content-Type and boundary expression
if (requestInit.body instanceof FormData) {
requestInit.headers.delete('Content-Type');
}

let request = new Request(url, requestInit);

Expand Down
6 changes: 5 additions & 1 deletion packages/client-fetch/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export { client, createClient } from '../';
export type { Client, Options, RequestResult } from '../types';
export { formDataBodySerializer, jsonBodySerializer } from '../utils';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../utils';
51 changes: 40 additions & 11 deletions packages/client-fetch/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,43 +460,72 @@ export const createInterceptors = <Req, Res, Options>() => ({
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});

const serializeFormDataPair = (
formData: FormData,
key: string,
value: unknown,
) => {
const serializeFormDataPair = (data: FormData, key: string, value: unknown) => {
if (typeof value === 'string' || value instanceof Blob) {
formData.append(key, value);
data.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
data.append(key, JSON.stringify(value));
}
};

export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const formData = new FormData();
const data = new FormData();

Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(formData, key, v));
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(formData, key, value);
serializeFormDataPair(data, key, value);
}
});

return formData;
return data;
},
};

export const jsonBodySerializer = {
bodySerializer: <T>(body: T) => JSON.stringify(body),
};

const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
) => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};

export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new URLSearchParams();

Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});

return data;
},
};

const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
Expand Down
35 changes: 27 additions & 8 deletions packages/openapi-ts/src/generate/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,33 @@ const toRequestOptions = (
.map((parameter) => parameter.mediaType)
.filter(Boolean)
.filter(unique);
if (contents.length === 1 && contents[0] === 'multipart/form-data') {
obj = [
...obj,
{
spread: 'formDataBodySerializer',
},
];
onClientImport?.('formDataBodySerializer');
if (contents.length === 1) {
if (contents[0] === 'multipart/form-data') {
obj = [
...obj,
{
spread: 'formDataBodySerializer',
},
// no need for Content-Type header, browser will set it automatically
];
onClientImport?.('formDataBodySerializer');
}

if (contents[0] === 'application/x-www-form-urlencoded') {
obj = [
...obj,
{
spread: 'urlSearchParamsBodySerializer',
},
{
key: 'headers',
value: {
'Content-Type': contents[0],
},
},
];
onClientImport?.('urlSearchParamsBodySerializer');
}
}

// TODO: set parseAs to skip inference if every result has the same
Expand Down
21 changes: 14 additions & 7 deletions packages/openapi-ts/src/templates/core/ApiRequestOptions.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | Record<string, unknown>[];
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly errors?: Record<number | string, string>;
readonly url: string;
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | Record<string, unknown>[];
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly errors?: Record<number | string, string>;
readonly url: string;
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | Record<string, unknown>[];
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly errors?: Record<number | string, string>;
readonly url: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { ImportData, ImportResponse, ApiVversionOdataControllerCountResponse, DeleteFooData3, CallWithParametersData, CallWithWeirdParameterNamesData, GetCallWithOptionalParamData, PostCallWithOptionalParamData, CallWithDescriptionsData, DeprecatedCallData, PostApiRequestBodyData, PostApiFormDataData, CallWithDefaultParametersData, CallWithDefaultOptionalParametersData, CallToTestOrderOfParamsData, CallWithNoContentResponseResponse, CallWithResponseAndNoContentResponseResponse, CallWithResponseResponse, CallWithDuplicateResponsesResponse, CallWithResponsesResponse, DummyAResponse, DummyBResponse, CollectionFormatData, TypesData, TypesResponse, UploadFileData, UploadFileResponse, FileResponseData, FileResponseResponse, ComplexTypesData, ComplexTypesResponse, ComplexParamsData, ComplexParamsResponse, MultipartRequestData, MultipartResponseResponse, CallWithResultFromHeaderResponse, TestErrorCodeData, TestErrorCodeResponse, NonAsciiæøåÆøÅöôêÊ字符串Data, NonAsciiæøåÆøÅöôêÊ字符串Response } from './types.gen';
import type { ImportData, ImportResponse, ApiVversionOdataControllerCountResponse, DeleteFooData3, CallWithParametersData, CallWithWeirdParameterNamesData, GetCallWithOptionalParamData, PostCallWithOptionalParamData, CallWithDescriptionsData, DeprecatedCallData, PostApiRequestBodyData, PostApiFormDataData, CallWithDefaultParametersData, CallWithDefaultOptionalParametersData, CallToTestOrderOfParamsData, CallWithNoContentResponseResponse, CallWithResponseAndNoContentResponseResponse, CallWithResponseResponse, CallWithDuplicateResponsesResponse, CallWithResponsesResponse, DummyAResponse, DummyBResponse, CollectionFormatData, TypesData, TypesResponse, UploadFileData, UploadFileResponse, FileResponseData, FileResponseResponse, ComplexTypesData, ComplexTypesResponse, ComplexParamsData, ComplexParamsResponse, MultipartRequestData, MultipartResponseResponse, CallWithResultFromHeaderResponse, TestErrorCodeData, TestErrorCodeResponse, NonAsciiæøåÆøÅöôêÊ字符串Data, NonAsciiæøåÆøÅöôêÊ字符串Response, PutWithFormUrlEncodedData } from './types.gen';

export class DefaultService {
/**
Expand Down Expand Up @@ -859,4 +859,19 @@ export class NonAsciiÆøåÆøÅöôêÊService {
});
}

/**
* Login User
* @param data The data for the request.
* @param data.formData
* @throws ApiError
*/
public static putWithFormUrlEncoded(data: PutWithFormUrlEncodedData): CancelablePromise<void> {
return __request(OpenAPI, {
method: 'PUT',
url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded'
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,10 @@ export type NonAsciiæøåÆøÅöôêÊ字符串Data = {

export type NonAsciiæøåÆøÅöôêÊ字符串Response = Array<NonAsciiStringæøåÆØÅöôêÊ字符串>;

export type PutWithFormUrlEncodedData = {
formData: ArrayWithStrings;
};

export type $OpenApiTs = {
'/api/v{api-version}/no-tag': {
post: {
Expand Down Expand Up @@ -1784,5 +1788,8 @@ export type $OpenApiTs = {
200: Array<NonAsciiStringæøåÆØÅöôêÊ字符串>;
};
};
put: {
req: PutWithFormUrlEncodedData;
};
};
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, unknown>;
readonly body?: any;
readonly cookies?: Record<string, unknown>;
readonly errors?: Record<number | string, string>;
readonly formData?: Record<string, unknown> | Record<string, unknown>[];
readonly headers?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly formData?: Record<string, unknown>;
readonly body?: any;
readonly mediaType?: string;
readonly method:
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT';
readonly path?: Record<string, unknown>;
readonly query?: Record<string, unknown>;
readonly responseHeader?: string;
readonly responseTransformer?: (data: unknown) => Promise<T>;
readonly errors?: Record<number | string, string>;
readonly url: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HttpClient } from '@angular/common/http';
import type { Observable } from 'rxjs';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { ImportData, ImportResponse, ApiVversionOdataControllerCountResponse, DeleteFooData3, CallWithParametersData, CallWithWeirdParameterNamesData, GetCallWithOptionalParamData, PostCallWithOptionalParamData, CallWithDescriptionsData, DeprecatedCallData, PostApiRequestBodyData, PostApiFormDataData, CallWithDefaultParametersData, CallWithDefaultOptionalParametersData, CallToTestOrderOfParamsData, CallWithNoContentResponseResponse, CallWithResponseAndNoContentResponseResponse, CallWithResponseResponse, CallWithDuplicateResponsesResponse, CallWithResponsesResponse, DummyAResponse, DummyBResponse, CollectionFormatData, TypesData, TypesResponse, UploadFileData, UploadFileResponse, FileResponseData, FileResponseResponse, ComplexTypesData, ComplexTypesResponse, ComplexParamsData, ComplexParamsResponse, MultipartRequestData, MultipartResponseResponse, CallWithResultFromHeaderResponse, TestErrorCodeData, TestErrorCodeResponse, NonAsciiæøåÆøÅöôêÊ字符串Data, NonAsciiæøåÆøÅöôêÊ字符串Response } from './types.gen';
import type { ImportData, ImportResponse, ApiVversionOdataControllerCountResponse, DeleteFooData3, CallWithParametersData, CallWithWeirdParameterNamesData, GetCallWithOptionalParamData, PostCallWithOptionalParamData, CallWithDescriptionsData, DeprecatedCallData, PostApiRequestBodyData, PostApiFormDataData, CallWithDefaultParametersData, CallWithDefaultOptionalParametersData, CallToTestOrderOfParamsData, CallWithNoContentResponseResponse, CallWithResponseAndNoContentResponseResponse, CallWithResponseResponse, CallWithDuplicateResponsesResponse, CallWithResponsesResponse, DummyAResponse, DummyBResponse, CollectionFormatData, TypesData, TypesResponse, UploadFileData, UploadFileResponse, FileResponseData, FileResponseResponse, ComplexTypesData, ComplexTypesResponse, ComplexParamsData, ComplexParamsResponse, MultipartRequestData, MultipartResponseResponse, CallWithResultFromHeaderResponse, TestErrorCodeData, TestErrorCodeResponse, NonAsciiæøåÆøÅöôêÊ字符串Data, NonAsciiæøåÆøÅöôêÊ字符串Response, PutWithFormUrlEncodedData } from './types.gen';

@Injectable({
providedIn: 'root'
Expand Down Expand Up @@ -976,4 +976,19 @@ export class NonAsciiÆøåÆøÅöôêÊService {
});
}

/**
* Login User
* @param data The data for the request.
* @param data.formData
* @throws ApiError
*/
public putWithFormUrlEncoded(data: PutWithFormUrlEncodedData): Observable<void> {
return __request(OpenAPI, this.http, {
method: 'PUT',
url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串',
formData: data.formData,
mediaType: 'application/x-www-form-urlencoded'
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,10 @@ export type NonAsciiæøåÆøÅöôêÊ字符串Data = {

export type NonAsciiæøåÆøÅöôêÊ字符串Response = Array<NonAsciiStringæøåÆØÅöôêÊ字符串>;

export type PutWithFormUrlEncodedData = {
formData: ArrayWithStrings;
};

export type $OpenApiTs = {
'/api/v{api-version}/no-tag': {
post: {
Expand Down Expand Up @@ -1661,5 +1665,8 @@ export type $OpenApiTs = {
200: Array<NonAsciiStringæøåÆØÅöôêÊ字符串>;
};
};
put: {
req: PutWithFormUrlEncodedData;
};
};
};
Loading

0 comments on commit f6be838

Please sign in to comment.