openapi-diff
openapi-diff copied to clipboard
False negative when adding a new response status code to an endpoint
Adding a new possible response status to an endpoint is not flagged as a breaking change.
Old OpenAPI document
{
"openapi": "3.0.0",
"info": {
"title": "API to manipulate a counter",
"version": "1.0.0"
},
"paths": {
"/counter": {
"get": {
"responses": {
"200": {
"description": "The counter current value",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Counter"
}
}
}
},
"400": {
"description": "Client error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
}
}
},
"post": {
"responses": {
"200": {
"description": "The counter current value",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Counter"
}
}
}
},
"400": {
"description": "Client error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Operation"
}
}
},
"description": "The operation to apply to the counter"
}
}
}
},
"components": {
"schemas": {
"counter.Operation.Set": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Set"
],
"example": "Set"
},
"value": {
"type": "integer",
"format": "int32"
}
},
"required": [
"type",
"value"
]
},
"counter.Counter": {
"type": "object",
"properties": {
"value": {
"type": "integer",
"format": "int32"
}
},
"required": [
"value"
]
},
"counter.Operation": {
"oneOf": [
{
"$ref": "#/components/schemas/counter.Operation.Add"
},
{
"$ref": "#/components/schemas/counter.Operation.Set"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"Add": "#/components/schemas/counter.Operation.Add",
"Set": "#/components/schemas/counter.Operation.Set"
}
}
},
"counter.Operation.Add": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Add"
],
"example": "Add"
},
"delta": {
"type": "integer",
"format": "int32"
}
},
"required": [
"type",
"delta"
]
},
"endpoints.Errors": {
"type": "array",
"items": {
"type": "string"
}
}
},
"securitySchemes": {}
}
}
Note that the endpoint GET /counter
can only return statuses 200, 400, or 500.
New OpenAPI document
{
"openapi": "3.0.0",
"info": {
"title": "API to manipulate a counter",
"version": "1.0.0"
},
"paths": {
"/counter": {
"get": {
"responses": {
"200": {
"description": "The counter current value",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Counter"
}
}
}
},
"400": {
"description": "Client error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
},
"404": {
"description": "Counter not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
}
}
},
"post": {
"responses": {
"200": {
"description": "The counter current value",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Counter"
}
}
}
},
"400": {
"description": "Client error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/endpoints.Errors"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/counter.Operation"
}
}
},
"description": "The operation to apply to the counter"
}
}
}
},
"components": {
"schemas": {
"counter.Operation.Set": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Set"
],
"example": "Set"
},
"value": {
"type": "integer",
"format": "int32"
}
},
"required": [
"type",
"value"
]
},
"counter.Counter": {
"type": "object",
"properties": {
"value": {
"type": "integer",
"format": "int32"
}
},
"required": [
"value"
]
},
"counter.Operation": {
"oneOf": [
{
"$ref": "#/components/schemas/counter.Operation.Add"
},
{
"$ref": "#/components/schemas/counter.Operation.Set"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"Add": "#/components/schemas/counter.Operation.Add",
"Set": "#/components/schemas/counter.Operation.Set"
}
}
},
"counter.Operation.Add": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Add"
],
"example": "Add"
},
"delta": {
"type": "integer",
"format": "int32"
}
},
"required": [
"type",
"delta"
]
},
"endpoints.Errors": {
"type": "array",
"items": {
"type": "string"
}
}
},
"securitySchemes": {}
}
}
Note that the endpoint GET /counter
can now also return a 404 status.
I expect this change to break backward compatibility, because clients that work with the old version of the OpenAPI document don’t know how to handle a response with the new status code.
Conversely, I’ve noted that removing a status code is incorrectly flagged as a breaking change although it is not: clients don’t care if they don’t receive the removed status code.
I don't think that adding a new response status code is a breaking change according to the spec https://datatracker.ietf.org/doc/html/rfc7231#section-6. Status codes are designed as extensible. Clients need to take this into account.
The Zalando REST guidelines come to the same conclusion: https://opensource.zalando.com/restful-api-guidelines/#compatibility
I argue that it is a breaking change. Let’s take an endpoint that returns either 500, 400, or 200. Clients expect only those values. Now, if we publish a new version of that endpoint that also returns 409, existing clients won’t be able to handle that status code (since they expect only 500, 400, or 200). We could say that they should fall back to a default way to handle errors (4xx codes), but that’s suboptimal, IMHO.
According to the "HTTP 1.1 Semantics and Contents" the fallback behavior is a MUST for clients:
HTTP status codes are extensible. HTTP clients are not required to understand the meaning of all registered status codes, though such understanding is obviously desirable. However, a client MUST understand the class of any status code, as indicated by the first digit, and treat an unrecognized status code as being equivalent to the x00 status code of that class, with the exception that a recipient MUST NOT cache a response with an unrecognized status code.