Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-required-array-properties-in-response](#ibm-required-array-properties-in-response)
* [ibm-required-enum-properties-in-response](#ibm-required-enum-properties-in-response)
* [ibm-resource-response-consistency](#ibm-resource-response-consistency)
* [ibm-response-status-body](#ibm-response-status-body)
* [ibm-response-status-codes](#ibm-response-status-codes)
* [ibm-schema-casing-convention](#ibm-schema-casing-convention)
* [ibm-schema-description](#ibm-schema-description)
Expand Down Expand Up @@ -580,6 +581,12 @@ has non-form content. <b>This rule is disabled by default.</b></td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-response-status-body">ibm-response-status-body</a></td>
<td>error</td>
<td>Performs multiple checks on the operation response bodies based on status codes.</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-response-status-codes">ibm-response-status-codes</a></td>
<td>warn</td>
<td>Performs multiple checks on the status codes used in operation responses.</td>
Expand Down Expand Up @@ -5972,6 +5979,88 @@ paths:
</tr>
</table>

### ibm-response-status-body
<table>
<tr>
<td><b>Rule id:</b></td>
<td><b>ibm-response-status-body</b></td>
</tr>
<tr>
<td valign=top><b>Description:</b></td>
<td>This rule performs a few different checks on the operation response bodies based on status codes:
<ul>
<li>Regarding <code>30x</code> responses a response body should only accompany any <code>301</code>, <code>302</code>, <code>305</code>, <code>307</code>.</li>
<li>If a response body is provided for a 30x response, it must contain the following fields: <code>code</code>, <code>target</code> and <code>message</code>.</li>
<li>For a 30x response the code field must contain one of the following values for redirect code and nothing else: <code>forwarded</code>, <code>resolved</code>, <code>moved</code>, <code>remote_region</code>, <code>remote_account</code>, <code>version_mismatch</code>.</li>
</ul>
<p>References:
<ul>
<li><a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-status-codes">IBM Cloud API Handbook: Fundamentals/Status Codes</a></li>
</ul>
</td>
</tr>
<tr>
<td><b>Severity:</b></td>
<td>error</td>
</tr>
<tr>
<td><b>OAS Versions:</b></td>
<td>oas3</td>
</tr>
<tr>
<td valign=top><b>Compliant example:<b></td>
<td>
<pre>
paths:
'/v1/things':
post:
operationId: create_thing
description: 'Create a Thing instance.'
responses:
'301':
description: 'Only partial data'
content:
application/json:
schema:
$ref: '#/components/schemas/Thing'
code: 'remote_region'
message: 'The requested resource is in a different region'
target:
crn: 'crn:v1:bluemix:public:is:us-south-1:a/aa2432b1fa4d4ace891e9b80fc104e34::share:r134-a0c07083-f411-446c-9316-7b08d6448c86'
href: 'https://us-south.iaas.cloud.ibm.com/v1/shares/r134-a0c07083-f411-446c-9316-7b08d6448c86'
id: 'r134-a0c07083-f411-446c-9316-7b08d6448c86'
name: 'my-share'
remote:
region:
href: 'https://us-east.iaas.cloud.ibm.com/v1/regions/us-south'
name: 'us-south'
resource_type: 'region'
resource_type: 'share'
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Non-compliant example:<b></td>
<td>
<pre>
paths:
'/v1/things':
post:
operationId: create_thing
description: 'Create a Thing instance.'
responses:
'301':
description: 'Only partial data'
content:
application/json:
schema:
$ref: '#/components/schemas/Thing'
code: 'remote'
</pre>
</td>
</tr>
</table>


### ibm-response-status-codes
<table>
Expand Down Expand Up @@ -5999,6 +6088,7 @@ there is no body representation for the resource).</li>
or <code>202 - Accepted</code> status code.</li>
<li>A PATCH operation must return either a <code>200 - OK</code>
or a <code>202 - Accepted</code> status code.</li>
<li>Status codes <code>301</code>, <code>302</code>, <code>305</code>, <code>307</code> should include a response body.</li>
<p>Note that for the purposes of this rule, an operation is considered to be a "create"-type operation if the
operationId starts with "create" or the operation is a POST request and there is another path
present in the API that is similar to the path of the "create" operation, but with a trailing path parameter reference.
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ module.exports = {
requiredProperty: require('./required-property'),
resourceResponseConsistency: require('./resource-response-consistency'),
responseExampleExists: require('./response-example-exists'),
responseStatusBody: require('./response-status-body'),
responseStatusCodes: require('./response-status-codes'),
schemaCasingConvention: require('./schema-casing-convention'),
schemaDescriptionExists: require('./schema-description-exists'),
Expand Down
131 changes: 131 additions & 0 deletions packages/ruleset/src/functions/response-status-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright 2017 - 2025 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const { getResolvedSpec } = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory, getResponseCodes } = require('../utils');

let ruleId;
let logger;

module.exports = function (operation, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}

return responseStatusBody(operation, context.path, getResolvedSpec(context));
};

/**
* This function performs a few checks on each operation's response field:
* 1. Only status codes 301, 302, 305, 307 should include a response body for 30x response codes.
* 2. Every response body for status codes 30x should contain a value for code that matches on of the following values:
* forwarded, resolved, moved, remote_region, remote_account, version_mismatch.
* 3. Every response body for status codes 30x should include "code" property.
* 4. Every response body for status codes 30x should include "message" property.
* 5. Every response body for status codes 30x should include "target" property.
* @param {*} operation an operation within the API definition.
* @param {*} path the array of path segments indicating the "location" of the operation within the API definition.
*/
function responseStatusBody(operation, path) {
if (!operation.responses) {
return [];
}

logger.debug(
`${ruleId}: checking response bodies for operation at location: ${path.join(
'.'
)}`
);

const errors = [];

const [statusCodes] = getResponseCodes(operation.responses);

if (statusCodes.length) {
const responseCodesWithBody = ['301', '302', '305', '307'];
const redirectCodes = [
'forwarded',
'resolved',
'moved',
'remote_region',
'remote_account',
'version_mismatch',
];

const redirectResponses = statusCodes.filter(code => code.startsWith('3'));
if (redirectResponses.length) {
const responseCodes = redirectResponses.filter(
code => !responseCodesWithBody.includes(code)
);

// 1. Only status codes 301, 302, 305, 307 should include a response body for 30x response codes.
responseCodes.forEach(responseCode => {
const response = operation.responses[responseCode];
if (response && response.content) {
errors.push({
message:
'Only a 301, 302, 305, 307 response should include a response body',
path: [...path, 'responses'],
});
}
});

responseCodesWithBody.forEach(code => {
const response = operation.responses[code];
if (response && response.content) {
const applicationJson = response.content['application/json'];

if (!applicationJson) return;

const redirectCode = applicationJson['code'];
const message = applicationJson['message'];
const target = applicationJson['target'];

// 2. Every response body for status codes 30x should contain a value for code that matches one of the following values:
// forwarded, resolved, moved, remote_region, remote_account, version_mismatch.
// 3. Every response body for status codes 30x should include "code" property.
if (redirectCode) {
for (let i = 0; i < redirectCodes.length; i++) {
if (redirectCode == redirectCodes[i]) break;
else if (i === redirectCodes.length - 1) {
errors.push({
message: `Redirect code should match one of the following: ${redirectCodes.join(
' or '
)}`,
path: [...path, 'responses'],
});
}
}
} else {
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "code" field',
path: [...path, 'responses'],
});
}

// 4. Every response body for status codes 30x should include "message" property.
if (!message)
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "message" field',
path: [...path, 'responses'],
});

// 5. Every response body for status codes 30x should include "target" property.
if (!target)
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "target" field',
path: [...path, 'responses'],
});
}
});
}
}

return errors;
}
36 changes: 25 additions & 11 deletions packages/ruleset/src/functions/response-status-codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,18 @@ module.exports = function (operation, _opts, context) {

/**
* This function performs a few checks on each operation's responses field:
* 1. Status code 400 should be used instead of 422.
* 2. Status code 303 or 307 should be used instead of 302.
* 3. Operation responses should include at least one successful (2xx) status code.
* 4. Operation responses should not include status code 101 when successful (2xx) status codes are present.
* 5. A 204 response must not have content.
* 6. A "create" operation must return either a 201 or a 202 (or a 204, if the corresponding GET request returns a 204).
* An operation is considered to be a "create" operation if the operationId starts with "create"
* OR it's a POST request and there is a similar path but with a trailing path parameter reference.
* 7. A "PUT" operation must return either a 200, 201, or 202
* 8. A "PATCH" operation must return either a 200 or a 202
* 9. If an operation returns status code 202, it should not return any other 2xx status codes.
* 1. Status code 400 should be used instead of 422.
* 2. Status code 303 or 307 should be used instead of 302.
* 3. Operation responses should include at least one successful (2xx) status code.
* 4. Operation responses should not include status code 101 when successful (2xx) status codes are present.
* 5. A 204 response must not have content.
* 6. A "create" operation must return either a 201 or a 202 (or a 204, if the corresponding GET request returns a 204).
* An operation is considered to be a "create" operation if the operationId starts with "create"
* OR it's a POST request and there is a similar path but with a trailing path parameter reference.
* 7. A "PUT" operation must return either a 200, 201, or 202
* 8. A "PATCH" operation must return either a 200 or a 202
* 9. If an operation returns status code 202, it should not return any other 2xx status codes.
* 10. Status codes 301, 302, 305, 307 should include a response body.
* @param {*} operation an operation within the API definition
* @param {*} path the array of path segments indicating the "location" of the operation within the API definition
* @param {*} apidef the resolved API spec
Expand Down Expand Up @@ -160,6 +161,19 @@ function responseStatusCodes(operation, path, apidef) {
path: [...path, 'responses'],
});
}

//10. Status codes 301, 302, 305, 307 should include a response body.
['301', '302', '305', '307'].forEach(code => {
const response30x = operation.responses[code];

if (response30x && !response30x.content) {
errors.push({
message:
'A 301, 302, 305 or 307 response should include a response body, use a different status code for responses without content',
path: [...path, 'responses'],
});
}
});
}

if (errors.length) {
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/ibm-oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ module.exports = {
'ibm-required-enum-properties-in-response':
ibmRules.requiredEnumPropertiesInResponse,
'ibm-resource-response-consistency': ibmRules.resourceResponseConsistency,
'ibm-response-status-body': ibmRules.responseStatusBody,
'ibm-response-status-codes': ibmRules.responseStatusCodes,
'ibm-schema-casing-convention': ibmRules.schemaCasingConvention,
'ibm-schema-description': ibmRules.schemaDescriptionExists,
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ module.exports = {
requiredPropertyMissing: require('./required-property-missing'),
resourceResponseConsistency: require('./resource-response-consistency'),
responseExampleExists: require('./response-example-exists'),
responseStatusBody: require('./response-status-body'),
responseStatusCodes: require('./response-status-codes'),
schemaCasingConvention: require('./schema-casing-convention'),
schemaDescriptionExists: require('./schema-description-exists'),
Expand Down
23 changes: 23 additions & 0 deletions packages/ruleset/src/rules/response-status-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright 2017 - 2025 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const {
operations,
} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections');
const { oas3 } = require('@stoplight/spectral-formats');
const { responseStatusBody } = require('../functions');

module.exports = {
description:
'Performs multiple checks on the response body based on status codes',
message: '{{error}}',
formats: [oas3],
given: operations,
severity: 'error',
resolved: true,
then: {
function: responseStatusBody,
},
};
Loading