Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
69 changes: 69 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)
Comment thread
diatrcz marked this conversation as resolved.
Outdated
* [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,68 @@ 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, target and message.</li>
<li>For a 30x response the code field must contain one of the following values for redirect code and nothing else: forwarded, resolved, moved, remote_region, remote_account, version_mismatch.</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>Non-compliant example:<b></td>
Comment thread
diatrcz marked this conversation as resolved.
Outdated
<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',
Comment thread
diatrcz marked this conversation as resolved.
Outdated
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>
</table>


### ibm-response-status-codes
<table>
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
127 changes: 127 additions & 0 deletions packages/ruleset/src/functions/response-status-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* 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:
Comment thread
diatrcz marked this conversation as resolved.
Outdated
* 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.
*/

Comment thread
diatrcz marked this conversation as resolved.
Outdated
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 responses = statusCodes.filter(code => code.startsWith('3'));
Comment thread
diatrcz marked this conversation as resolved.
Outdated
if (responses.length) {
const responseCode = responses.filter(
code => !responseCodesWithBody.includes(code)
);
const response = operation.responses[responseCode];
Comment thread
diatrcz marked this conversation as resolved.
Outdated

// 1. Only status codes 301, 302, 305, 307 should include a response body for 30x response codes.
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'];
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 on of the following values:
Comment thread
diatrcz marked this conversation as resolved.
Outdated
// 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 "message" property.
Comment thread
diatrcz marked this conversation as resolved.
Outdated
if (!target)
errors.push({
message:
'Response body for response codes 301, 302, 305 and 307 should include "target" field',
path: [...path, 'responses'],
});
}
});
}
}

return errors;
}
40 changes: 29 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,23 @@ function responseStatusCodes(operation, path, apidef) {
path: [...path, 'responses'],
});
}

//10. Status codes 301, 302, 305, 307 should include a response body.
/*const response30x = ['301', '302', '305', '307'].find(
code => operation.responses[code]
);*/
Comment thread
diatrcz marked this conversation as resolved.
Outdated

['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