mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
fix(API): Require a JSON content type on decorator routes that take a body (#36748)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { ResponseError } from './abstract/response.error';
|
||||
|
||||
export class UnsupportedMediaTypeError extends ResponseError {
|
||||
constructor(message: string, hint: string | undefined = undefined) {
|
||||
super(message, 415, 415, hint);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export function markPublicApiController(controllerClass: Controller, basePath: `
|
||||
}
|
||||
|
||||
export class WidgetBodyDto extends Z.class({ name: z.string() }) {}
|
||||
export class OptionalWidgetBodyDto extends Z.class({ name: z.string().optional() }) {}
|
||||
export class WidgetQueryDto extends Z.class({ q: z.string().optional() }) {}
|
||||
export class WidgetResponseDto extends Z.class({ id: z.string() }) {}
|
||||
/** Query DTO carrying a shared pagination field, for tests asserting `SHARED_PAGINATION_PARAMS` handling. */
|
||||
|
||||
@@ -15,7 +15,11 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { EventService } from '@/events/event.service';
|
||||
import { markPublicApiController } from '@/public-api/__tests__/public-api-controller-test-utils';
|
||||
import {
|
||||
markPublicApiController,
|
||||
OptionalWidgetBodyDto,
|
||||
WidgetBodyDto,
|
||||
} from '@/public-api/__tests__/public-api-controller-test-utils';
|
||||
import { PublicApiControllerRegistry } from '@/public-api/public-api-controller.registry';
|
||||
import type { AuthStrategyRegistry } from '@/services/auth-strategy.registry';
|
||||
import type { LastActiveAtService } from '@/services/last-active-at.service';
|
||||
@@ -28,6 +32,11 @@ describe('PublicApiControllerRegistry', () => {
|
||||
function activate(): express.Express {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
// mirrors the app-wide bodyParser, which defaults an absent body to `{}`
|
||||
app.use((req, _res, next) => {
|
||||
req.body ??= {};
|
||||
next();
|
||||
});
|
||||
const router = express.Router({ mergeParams: true });
|
||||
new PublicApiControllerRegistry(
|
||||
Container.get(ControllerRegistryMetadata),
|
||||
@@ -101,7 +110,7 @@ describe('PublicApiControllerRegistry', () => {
|
||||
});
|
||||
|
||||
describe('validation failures', () => {
|
||||
class WidgetBodyDto extends Z.class({
|
||||
class WidgetValidationDto extends Z.class({
|
||||
name: z.string(),
|
||||
active: z.undefined({ invalid_type_error: 'is read-only' }),
|
||||
}) {}
|
||||
@@ -111,7 +120,7 @@ describe('PublicApiControllerRegistry', () => {
|
||||
class WidgetsPublicController {
|
||||
@Post('/')
|
||||
@ApiResponse(200)
|
||||
create(_req: express.Request, _res: express.Response, @Body _body: WidgetBodyDto) {
|
||||
create(_req: express.Request, _res: express.Response, @Body _body: WidgetValidationDto) {
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
@@ -125,4 +134,121 @@ describe('PublicApiControllerRegistry', () => {
|
||||
expect(response.body.message).toBe('request/body/active is read-only');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request media type', () => {
|
||||
function registerOptionalBodyRoute() {
|
||||
@Service()
|
||||
class WidgetsPublicController {
|
||||
@Post('/')
|
||||
@ApiResponse(200)
|
||||
method(_req: unknown, _res: unknown, @Body body: OptionalWidgetBodyDto) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
markPublicApiController(WidgetsPublicController as Controller, '/widgets');
|
||||
}
|
||||
|
||||
function registerBodyRoute() {
|
||||
@Service()
|
||||
class WidgetsPublicController {
|
||||
@Post('/')
|
||||
@ApiResponse(200)
|
||||
method(_req: unknown, _res: unknown, @Body body: WidgetBodyDto) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
markPublicApiController(WidgetsPublicController as Controller, '/widgets');
|
||||
}
|
||||
|
||||
it('accepts application/json', async () => {
|
||||
registerBodyRoute();
|
||||
|
||||
await request(activate())
|
||||
.post('/widgets')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({ name: 'a' })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('accepts application/json with parameters', async () => {
|
||||
registerBodyRoute();
|
||||
|
||||
await request(activate())
|
||||
.post('/widgets')
|
||||
.set('Content-Type', 'application/json; charset=utf-8')
|
||||
.send({ name: 'a' })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['application/x-www-form-urlencoded', 'application/x-www-form-urlencoded'],
|
||||
['application/xml', 'application/xml'],
|
||||
['text/plain', 'text/plain'],
|
||||
['application/octet-stream', 'application/octet-stream'],
|
||||
['text/PlAiN; charset=UTF-8', 'text/plain; charset=utf-8'],
|
||||
['multipart/form-data; boundary=XYZ', 'multipart/form-data'],
|
||||
])('rejects %s with 415', async (sent, reported) => {
|
||||
registerBodyRoute();
|
||||
|
||||
const response = await request(activate())
|
||||
.post('/widgets')
|
||||
.set('Content-Type', sent)
|
||||
.send('a')
|
||||
.expect(415);
|
||||
|
||||
expect(response.body.message).toBe(`unsupported media type ${reported}`);
|
||||
});
|
||||
|
||||
it('rejects a non-JSON media type when no body follows', async () => {
|
||||
registerBodyRoute();
|
||||
|
||||
const response = await request(activate())
|
||||
.post('/widgets')
|
||||
.set('Content-Type', 'application/x-www-form-urlencoded')
|
||||
.expect(415);
|
||||
|
||||
expect(response.body.message).toBe(
|
||||
'unsupported media type application/x-www-form-urlencoded',
|
||||
);
|
||||
});
|
||||
|
||||
const namesNoMediaType: Array<[string, string | undefined]> = [
|
||||
['a request with no Content-Type', undefined],
|
||||
['a request with an empty Content-Type', ''],
|
||||
['a request with a whitespace Content-Type', ' '],
|
||||
];
|
||||
|
||||
function postWithContentType(header: string | undefined) {
|
||||
const pending = request(activate()).post('/widgets');
|
||||
|
||||
return header === undefined ? pending : pending.set('Content-Type', header);
|
||||
}
|
||||
|
||||
it.each(namesNoMediaType)(
|
||||
'accepts %s when every body field is optional',
|
||||
async (_label, header) => {
|
||||
registerOptionalBodyRoute();
|
||||
|
||||
await postWithContentType(header).expect(200);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(namesNoMediaType)('rejects %s when the body is required', async (_label, header) => {
|
||||
registerBodyRoute();
|
||||
|
||||
const response = await postWithContentType(header).expect(415);
|
||||
|
||||
expect(response.body.message).toBe('unsupported media type undefined');
|
||||
});
|
||||
|
||||
it('accepts application/json carrying an unrelated parameter', async () => {
|
||||
registerBodyRoute();
|
||||
|
||||
await request(activate())
|
||||
.post('/widgets')
|
||||
.set('Content-Type', 'application/json; Foo=BAR')
|
||||
.send({ name: 'a' })
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,11 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { License } from '@/license';
|
||||
import { userHasScopes } from '@/permissions.ee/check-access';
|
||||
import { assertJsonContentType } from '@/public-api/public-api-media-type';
|
||||
import {
|
||||
apiKeyScopesSatisfy,
|
||||
isDtoArg,
|
||||
isRequestBodyRequired,
|
||||
resolveRouteArgs,
|
||||
resolveSuccessStatus,
|
||||
} from '@/public-api/public-api-route-resolver';
|
||||
@@ -59,7 +62,12 @@ export class PublicApiControllerRegistry {
|
||||
route.successStatus,
|
||||
);
|
||||
|
||||
const bodyDto = resolvedArgs.find((arg) => isDtoArg(arg, 'body'))?.dto;
|
||||
const bodyRequired = bodyDto ? isRequestBodyRequired(bodyDto) : false;
|
||||
|
||||
const handler = async (req: Request, res: Response) => {
|
||||
if (bodyDto) assertJsonContentType(req.headers['content-type'], bodyRequired);
|
||||
|
||||
const args: unknown[] = [req, res];
|
||||
for (const arg of resolvedArgs) {
|
||||
if (arg.type === 'param') {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { UnsupportedMediaTypeError } from '@/errors/response-errors/unsupported-media-type.error';
|
||||
|
||||
const JSON_MEDIA_TYPE = 'application/json';
|
||||
|
||||
/**
|
||||
* The media type, plus the string the legacy validator reports: lower-cased, parameters sorted by
|
||||
* name, `boundary` left out.
|
||||
*/
|
||||
function readMediaType(header: string): { mediaType: string; reported: string } {
|
||||
const [rawMediaType, ...parameterParts] = header.split(';');
|
||||
const mediaType = rawMediaType.trim().toLowerCase();
|
||||
const parameters = new Map<string, string>();
|
||||
|
||||
for (const part of parameterParts) {
|
||||
const separator = part.indexOf('=');
|
||||
if (separator === -1) continue;
|
||||
|
||||
const name = part.slice(0, separator).trim().toLowerCase();
|
||||
if (name === 'boundary') continue;
|
||||
|
||||
const value = part.slice(separator + 1);
|
||||
parameters.set(name, name === 'charset' ? value.toLowerCase() : value);
|
||||
}
|
||||
|
||||
const reported = [...parameters.entries()]
|
||||
.sort(([a], [b]) => (a < b ? -1 : 1))
|
||||
.reduce((out, [name, value]) => `${out}; ${name}=${value}`, mediaType);
|
||||
|
||||
return { mediaType, reported };
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy validator accepted only JSON. It reported a header that names no media type — absent,
|
||||
* empty, or whitespace — as the literal `undefined`, and rejected it only when the body was
|
||||
* required. Migrated routes keep both behaviours and the messages that came with them.
|
||||
*/
|
||||
export function assertJsonContentType(header: string | undefined, bodyRequired: boolean): void {
|
||||
const { mediaType, reported } = readMediaType(header ?? '');
|
||||
|
||||
if (mediaType === '') {
|
||||
if (bodyRequired) throw new UnsupportedMediaTypeError('unsupported media type undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaType !== JSON_MEDIA_TYPE) {
|
||||
throw new UnsupportedMediaTypeError(`unsupported media type ${reported}`);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export type ResolvedRouteArg =
|
||||
| { type: 'param'; key: string }
|
||||
| { type: 'body' | 'query'; dto: ZodClass };
|
||||
|
||||
function isDtoArg(
|
||||
export function isDtoArg(
|
||||
arg: ResolvedRouteArg,
|
||||
type: 'body' | 'query',
|
||||
): arg is Extract<ResolvedRouteArg, { type: 'body' | 'query' }> {
|
||||
@@ -116,6 +116,14 @@ export function resolveRouteArgs(
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a caller must send a body: an empty object being invalid means one is needed. Mirrors
|
||||
* `requestBody.required` in the hand-written specs, without a second place to declare it.
|
||||
*/
|
||||
export function isRequestBodyRequired(dto: ZodClass): boolean {
|
||||
return !dto.safeParse({}).success;
|
||||
}
|
||||
|
||||
/** Every decorator route must state its success status via `@ApiResponse`. */
|
||||
export function resolveSuccessStatus(
|
||||
controllerName: string,
|
||||
|
||||
+2
@@ -38,3 +38,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -57,3 +58,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+2
@@ -61,3 +61,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -51,3 +52,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -14,6 +14,7 @@ parameters:
|
||||
name: roleMappingRuleId
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -39,3 +40,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+2
@@ -45,3 +45,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
@@ -8,6 +8,7 @@ x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -46,3 +47,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
@@ -14,6 +14,7 @@ parameters:
|
||||
name: slug
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -50,3 +51,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -1198,5 +1199,7 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
'422':
|
||||
$ref: ../../../../shared/spec/responses/unprocessableEntity.yml
|
||||
|
||||
+2
@@ -578,3 +578,5 @@ responses:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -14,6 +14,7 @@ parameters:
|
||||
name: workflowId
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -34,3 +35,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
+3
@@ -14,6 +14,7 @@ parameters:
|
||||
name: workflowId
|
||||
in: path
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -41,3 +42,5 @@ responses:
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
'415':
|
||||
$ref: ../../../../shared/spec/responses/unsupportedMediaType.yml
|
||||
|
||||
@@ -20,6 +20,7 @@ import { z } from 'zod';
|
||||
|
||||
import {
|
||||
markPublicApiController,
|
||||
OptionalWidgetBodyDto,
|
||||
WidgetArrayResponseDto,
|
||||
WidgetBodyDto,
|
||||
WidgetPaginationQueryDto,
|
||||
@@ -133,6 +134,7 @@ describe('getDecoratorGeneratedOperations', () => {
|
||||
expect(params?.shape).toHaveProperty('id');
|
||||
expect(operation.config.request?.query).toBeDefined();
|
||||
expect(operation.config.request?.body).toEqual({
|
||||
required: true,
|
||||
content: { 'application/json': { schema: WidgetBodyDto.schema } },
|
||||
});
|
||||
});
|
||||
@@ -154,6 +156,7 @@ describe('getDecoratorGeneratedOperations', () => {
|
||||
expect(operation.config.parameters).toBeUndefined();
|
||||
expect(operation.config.request).toBeUndefined();
|
||||
expect(operation.config.deprecated).toBeUndefined();
|
||||
expect(operation.config.responses[415]).toBeUndefined();
|
||||
expect(operation.config.responses[200]).toEqual({ description: 'Operation successful.' });
|
||||
expect(operation.config.responses[401]).toEqual({
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml',
|
||||
@@ -196,6 +199,34 @@ describe('getDecoratorGeneratedOperations', () => {
|
||||
expect(() => getDecoratorGeneratedOperations()).toThrow(/ApiErrorResponse\(418\)/);
|
||||
});
|
||||
|
||||
it('leaves the request body optional when every field is optional', () => {
|
||||
class WidgetsPublicController {
|
||||
@Post('/')
|
||||
@ApiResponse(200)
|
||||
method(_req: unknown, _res: unknown, @Body _body: OptionalWidgetBodyDto) {}
|
||||
}
|
||||
markPublicApiController(WidgetsPublicController as Controller, '/widgets');
|
||||
|
||||
const [operation] = getDecoratorGeneratedOperations();
|
||||
|
||||
expect(operation.config.request?.body?.required).toBeUndefined();
|
||||
});
|
||||
|
||||
it('documents a 415 for a route that takes a request body', () => {
|
||||
class WidgetsPublicController {
|
||||
@Post('/')
|
||||
@ApiResponse(200)
|
||||
method(_req: unknown, _res: unknown, @Body _body: WidgetBodyDto) {}
|
||||
}
|
||||
markPublicApiController(WidgetsPublicController as Controller, '/widgets');
|
||||
|
||||
const [operation] = getDecoratorGeneratedOperations();
|
||||
|
||||
expect(operation.config.responses[415]).toEqual({
|
||||
$ref: '../../../../shared/spec/responses/unsupportedMediaType.yml',
|
||||
});
|
||||
});
|
||||
|
||||
it('refs the shared response file for an error status declared without a body DTO', () => {
|
||||
class WidgetsPublicController {
|
||||
@Get('/')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { z } from 'zod';
|
||||
|
||||
import type { ResolvedPublicApiRoute } from '@/public-api/public-api-route-resolver';
|
||||
import {
|
||||
isRequestBodyRequired,
|
||||
resolvePublicApiRoutes,
|
||||
scopeRequirementToString,
|
||||
toOpenApiPathTemplate,
|
||||
@@ -36,6 +37,7 @@ export const ERROR_RESPONSE_REFS = {
|
||||
403: { $ref: '../../../../shared/spec/responses/forbidden.yml' },
|
||||
404: { $ref: '../../../../shared/spec/responses/notFound.yml' },
|
||||
409: { $ref: '../../../../shared/spec/responses/conflict.yml' },
|
||||
415: { $ref: '../../../../shared/spec/responses/unsupportedMediaType.yml' },
|
||||
422: { $ref: '../../../../shared/spec/responses/unprocessableEntity.yml' },
|
||||
503: { $ref: '../../../../shared/spec/responses/serviceUnavailable.yml' },
|
||||
} as const satisfies Record<number, { $ref: string }>;
|
||||
@@ -57,6 +59,7 @@ export const ERROR_RESPONSE_DESCRIPTIONS: Record<DocumentedErrorStatus, string>
|
||||
403: 'Forbidden',
|
||||
404: 'The specified resource was not found.',
|
||||
409: 'Conflict',
|
||||
415: 'Unsupported media type.',
|
||||
422: 'Unprocessable Entity',
|
||||
503: 'The requested service is temporarily unavailable.',
|
||||
};
|
||||
@@ -156,6 +159,7 @@ function buildRequestBody(
|
||||
if (!route.requestBodyDto) return undefined;
|
||||
|
||||
return {
|
||||
...(isRequestBodyRequired(route.requestBodyDto) ? { required: true } : {}),
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: route.requestBodyDto.schema,
|
||||
@@ -215,6 +219,9 @@ function buildResponses(
|
||||
if (route.requestBodyDto ?? route.requestQueryDto) {
|
||||
responses[400] = ERROR_RESPONSE_REFS[400];
|
||||
}
|
||||
if (route.requestBodyDto) {
|
||||
responses[415] = ERROR_RESPONSE_REFS[415];
|
||||
}
|
||||
responses[401] = ERROR_RESPONSE_REFS[401];
|
||||
if (route.apiKeyScope) {
|
||||
responses[403] = ERROR_RESPONSE_REFS[403];
|
||||
|
||||
@@ -10,6 +10,8 @@ Forbidden:
|
||||
$ref: './forbidden.yml'
|
||||
PaymentRequired:
|
||||
$ref: './paymentRequired.yml'
|
||||
UnsupportedMediaType:
|
||||
$ref: './unsupportedMediaType.yml'
|
||||
UnprocessableEntity:
|
||||
$ref: './unprocessableEntity.yml'
|
||||
ServiceUnavailable:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
description: Unsupported media type.
|
||||
Reference in New Issue
Block a user