Skip to content

Commit

Permalink
Merge pull request #422 from V4Fire/custom-no-content-status
Browse files Browse the repository at this point in the history
Custom no content status
  • Loading branch information
kormanowsky authored Jul 25, 2024
2 parents e1f2ff0 + f738247 commit 5f1565a
Show file tree
Hide file tree
Showing 19 changed files with 134 additions and 30 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ Changelog
_Note: Gaps between patch versions are faulty, broken or test releases._

## v3.100.0-rc.3 (2024-07-25)

#### :rocket: New Feature

* Added a new option `noContentStatuses`. This option allows to pass custom status code, array or range of status codes
which indicate a no-content response. By default, an array `[...Range(100, 199), 204, 304]` is used, but it may be useful
to override this value if your backend uses different status codes for no-content responses. `core/request`

## v3.100.0-rc.2 (2024-07-02)

#### :bug: Bug Fix
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "lib/core/index.js",
"typings": "index.d.ts",
"license": "MIT",
"version": "3.100.0-rc.2",
"version": "3.100.0-rc.3",
"author": "kobezzza <[email protected]> (https://github.com/kobezzza)",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/core/data/middlewares/attach-mock/attach-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function attachMock(this: Provider, params: MiddlewareParams): Prom
status: customResponse.status ?? mock.status ?? 200,
responseType: customResponse.responseType ?? (<any>mock).responseType ?? opts.responseType,
okStatuses: opts.okStatuses,
noContentStatuses: opts.noContentStatuses,
decoder: mock.decoders === false ? undefined : customResponse.decoders ?? ctx.decoders,
headers: customResponse.headers ?? mock.headers
});
Expand Down
8 changes: 8 additions & 0 deletions src/core/request/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ Changelog
> - :house: [Internal]
> - :nail_care: [Polish]
## v3.100.0-rc.3 (2024-07-25)

#### :rocket: New Feature

* Added a new option `noContentStatuses`. This option allows to pass custom status code, array or range of status codes
which indicate a no-content response. By default, an array `[...Range(100, 199), 204, 304]` is used, but it may be useful
to override this value if your backend uses different status codes for no-content responses. `core/request`

## v3.93.1 (2023-03-14)

#### :bug: Bug Fix
Expand Down
18 changes: 18 additions & 0 deletions src/core/request/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,24 @@ request('//users', {
}).data.then(console.log);
```

#### [noContentStatuses = `[statusCodes.NO_CONTENT, statusCodes.NOT_MODIFIED].concat(new Range<number>(100, 199).toArray(1))`]

A list of status codes (or a single code) that match response with no content.
Also, you can pass a range of codes.

```js
import request from 'core/request';
import Range from 'core/range';

request('//users', {
noContentStatuses: [204, 304, 424]
}).data.then(console.log);

request('//users', {
noContentStatuses: new Range(420, 430)
}).data.then(console.log);
```

#### timeout

A value in milliseconds for a request timeout.
Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/composition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function compositionEngine(
important: requestOptions.important,
responseType: 'object',
okStatuses: requestOptions.okStatuses,
noContentStatuses: requestOptions.noContentStatuses,
status: statusCodes.OK,
decoder: requestOptions.decoders
}));
Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ const request: RequestEngine = (params) => {
important: p.important,

okStatuses: p.okStatuses,
noContentStatuses: p.noContentStatuses,
status: res.status,
statusText: res.statusText,

Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ const request: RequestEngine = (params) => {
important: p.important,

okStatuses: p.okStatuses,
noContentStatuses: p.noContentStatuses,
status: response.statusCode,
statusText: response.statusMessage,

Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/provider/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const availableParams = [
'query',
'headers',
'okStatuses',
'noContentStatuses',
'timeout',
'important',
'meta',
Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export default function createProviderEngine(
important: providerResponse.important,

okStatuses: providerResponse.okStatuses,
noContentStatuses: providerResponse.noContentStatuses,
status: providerResponse.status,
statusText: providerResponse.statusText,

Expand Down
6 changes: 3 additions & 3 deletions src/core/request/engines/provider/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Provider, ModelMethod } from 'core/data';

import type {

OkStatuses,
Statuses,

RequestBody,
RequestMethod,
Expand Down Expand Up @@ -40,8 +40,8 @@ export interface AvailableOptions {

timeout?: number;
contentType?: string;
okStatuses?: OkStatuses;

okStatuses?: Statuses;
noContentStatuses?: Statuses;
meta: Meta;
important?: boolean;

Expand Down
1 change: 1 addition & 0 deletions src/core/request/engines/xhr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const request: RequestEngine = (params) => {
important: p.important,

okStatuses: p.okStatuses,
noContentStatuses: p.noContentStatuses,
status: xhr.status,
statusText: xhr.statusText,

Expand Down
1 change: 1 addition & 0 deletions src/core/request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {

export * from 'core/request/helpers';
export * from 'core/request/interface';
export * from 'core/request/response/helpers';
export * from 'core/request/response/interface';

export { globalOpts, cache, pendingCache } from 'core/request/const';
Expand Down
16 changes: 13 additions & 3 deletions src/core/request/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type NormalizedRequestBody = Exclude<
number | boolean | Dictionary
>;

export type OkStatuses =
export type Statuses =
Range<number> |
StatusCodes |
StatusCodes[];
Expand Down Expand Up @@ -275,7 +275,16 @@ export interface CreateRequestOptions<D = unknown> {
*
* @default `new Range(200, 299)`
*/
okStatuses?: OkStatuses;
okStatuses?: Statuses;

/**
* A list of status codes (or a single code) that match response with no content.
* Also, you can pass a range of codes.
*
* @default `[statusCodes.NO_CONTENT, statusCodes.NOT_MODIFIED]
* .concat(new Range<number>(100, 199).toArray(1))`
*/
noContentStatuses?: Statuses;

/**
* Value in milliseconds for a request timeout
Expand Down Expand Up @@ -679,7 +688,8 @@ export interface RequestOptions {
readonly parent: AbortablePromise;

readonly timeout?: number;
readonly okStatuses?: OkStatuses;
readonly okStatuses?: Statuses;
readonly noContentStatuses?: Statuses;

readonly contentType?: string;
readonly responseType?: ResponseType;
Expand Down
23 changes: 13 additions & 10 deletions src/core/request/response/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@ import Range from 'core/range';
import statusCodes from 'core/status-codes';
import type { ResponseType } from 'core/request';

export const defaultResponseOpts = {
url: '',
redirected: false,
status: 200,
statusText: 'OK',
okStatuses: new Range(200, 299),
responseType: <ResponseType>'text',
headers: {}
};

/**
* Status codes that cannot contain any content according to the HTTP standard
*
* 1xx - https://tools.ietf.org/html/rfc7231#section-6.2
* 204 - https://tools.ietf.org/html/rfc7231#section-6.3.5
* 304 - https://tools.ietf.org/html/rfc7232#section-4.1
*
* TODO: https://github.com/V4Fire/Core/issues/421
*/
export const noContentStatusCodes: number[] =
[statusCodes.NO_CONTENT, statusCodes.NOT_MODIFIED]
.concat(new Range<number>(100, 199).toArray(1));

export const defaultResponseOpts = {
url: '',
redirected: false,
status: 200,
statusText: 'OK',
okStatuses: new Range(200, 299),
noContentStatuses: noContentStatusCodes,
responseType: <ResponseType>'text',
headers: {}
};
21 changes: 21 additions & 0 deletions src/core/request/response/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { StatusCodes } from 'core/status-codes';
import type { Statuses } from 'core/request/interface';
import Range from 'core/range';

/**
* Returns true if specified `statuses` contain specified `statusCode`
*
* @param statuses
* @param statusCode
*/
export function statusesContainStatus(statuses: Statuses, statusCode: StatusCodes): boolean {
if (statuses instanceof Range) {
return statuses.contains(statusCode);
}

if (Object.isArray(statuses)) {
return statuses.includes(statusCode);
}

return statuses === statusCode;
}
35 changes: 25 additions & 10 deletions src/core/request/response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@ import Parser, { Token } from 'core/json/stream/parser';
import { createControllablePromise } from 'core/promise';
import AbortablePromise from 'core/promise/abortable';

import Range from 'core/range';
import symbolGenerator from 'core/symbol';

import Headers from 'core/request/headers';
import { FormData, Blob } from 'core/request/engines';

import { defaultResponseOpts, noContentStatusCodes } from 'core/request/response/const';
import { defaultResponseOpts } from 'core/request/response/const';

import type {

OkStatuses,
Statuses,
RequestResponseChunk,

WrappedDecoder,
Expand All @@ -53,10 +52,13 @@ import type {

} from 'core/request/response/interface';

import { statusesContainStatus } from 'core/request/response/helpers';

export * from 'core/request/headers';

export * from 'core/request/response/const';
export * from 'core/request/response/interface';
export * from 'core/request/response/helpers';

export const
$$ = symbolGenerator();
Expand Down Expand Up @@ -130,11 +132,23 @@ export default class Response<
*/
readonly ok: boolean;

/**
* True if the response status matches with no content status codes
* (by default it should match range from 100 to 199, 204 or 304)
*/
readonly hasNoContent: boolean;

/**
* A list of status codes (or a single code) that match successful operation.
* Also, you can pass a range of codes.
*/
readonly okStatuses: OkStatuses;
readonly okStatuses: Statuses;

/**
* A list of status codes (or a single code) that match a response with no content.
* Also, you can pass a range of codes.
*/
readonly noContentStatuses: Statuses;

/**
* Set of response headers
Expand Down Expand Up @@ -225,15 +239,16 @@ export default class Response<
this.important = p.important;

const
ok = p.okStatuses;
ok = p.okStatuses,
noContent = p.noContentStatuses;

this.status = p.status;
this.okStatuses = ok;
this.noContentStatuses = noContent;
this.statusText = p.statusText;

this.ok = ok instanceof Range ?
ok.contains(this.status) :
Array.concat([], <number>ok).includes(this.status);
this.ok = statusesContainStatus(ok, this.status);
this.hasNoContent = statusesContainStatus(noContent, this.status);

this.headers = Object.freeze(new Headers(p.headers));

Expand Down Expand Up @@ -350,7 +365,7 @@ export default class Response<
let
data;

if (noContentStatusCodes.includes(this.status)) {
if (this.hasNoContent) {
data = null;

} else {
Expand Down Expand Up @@ -403,7 +418,7 @@ export default class Response<
let
stream;

if (noContentStatusCodes.includes(this.status)) {
if (this.hasNoContent) {
stream = [].values();

} else {
Expand Down
5 changes: 3 additions & 2 deletions src/core/request/response/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { DataType } from 'core/mime-type';

import type {

OkStatuses,
Statuses,

WrappedDecoder,
WrappedDecoders,
Expand Down Expand Up @@ -76,7 +76,8 @@ export interface ResponseOptions {

status?: StatusCodes;
statusText?: string;
okStatuses?: OkStatuses;
okStatuses?: Statuses;
noContentStatuses?: Statuses;

responseType?: ResponseType;
forceResponseType?: boolean;
Expand Down
14 changes: 13 additions & 1 deletion src/core/request/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class TestRequestChainProvider extends Provider {

const
emptyBodyStatuses = [204, 304],
customEmptyBodyStatuses = [222, 323, 424, 525],
faviconBase64 = 'AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAnISL6JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL5JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL1JyEi9ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEihCchIpgnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEixCUgIRMmICEvJyEi5ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIrYnISKSJyEi9ichIlxQREUAHxobAichIo4nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISJeJyEiICchIuAnISJJJiAhbCYgITgmICEnJyEi4ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEiXichIiAnISLdJyEihichIuknISKkIRwdBCchIoUnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIl4nISIgJyEi4ichIu4nISL/JyEi8CYgIT8mICEhJyEi3CchIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISJeJyEiHychIuUnISL/JyEi/ychIv8nISKrIh0eBiYhInwnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEiXSYgITUnISLvJyEi/ychIv8nISL/JyEi9CYgIUcmICEbJyEi1ichIv8nISL/JyEi/ychIv8nISL/JyEi/ichImknISKjJyEi/ychIv8nISL/JyEi/ychIv8nISKzIRwdBiYhIX0nISL/JyEi/ychIv8nISL/JyEi/ychIvwnISK+JyEi9CchIv8nISL/JyEi/ychIv8nISL/JyEi9yYhIoonISKzJyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL6JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==';

describe('core/request', () => {
Expand Down Expand Up @@ -460,6 +461,17 @@ describe('core/request', () => {
expect(await req.data).toBe(null);
});
}

for (const status of customEmptyBodyStatuses) {
it(`response with ${status} status included in custom no-content statuses`, async () => {
const req = await request(`http://localhost:4000/octet/${status}`, {
okStatuses: status,
noContentStatuses: status
});

expect(await req.data).toBe(null);
});
}
});

it('retrying of a request', async () => {
Expand Down Expand Up @@ -795,7 +807,7 @@ function createServer() {
res.send(Buffer.from(faviconBase64, 'base64'));
});

for (const status of emptyBodyStatuses) {
for (const status of [].concat(emptyBodyStatuses, customEmptyBodyStatuses)) {
serverApp.get(`/octet/${status}`, (req, res) => {
res.type('application/octet-stream').status(status).end();
});
Expand Down

0 comments on commit 5f1565a

Please sign in to comment.