Use precedence for OpenAPI servers (#4020)

This commit is contained in:
Nolann B.
2026-02-18 01:20:39 +01:00
committed by GitHub
parent e73d9afd86
commit 11d9b80e77
4 changed files with 132 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": patch
---
Use precedence for OpenAPI servers
@@ -0,0 +1,44 @@
{
"openapi": "3.0.0",
"info": {
"title": "Server Precedence Test",
"version": "1.0.0"
},
"servers": [{ "url": "https://root.example.com" }],
"paths": {
"/root-only": {
"get": {
"operationId": "rootOnly",
"responses": { "200": { "description": "OK" } }
}
},
"/path-override": {
"servers": [{ "url": "https://path.example.com" }],
"get": {
"operationId": "pathOverride",
"responses": { "200": { "description": "OK" } }
}
},
"/operation-override": {
"servers": [{ "url": "https://path.example.com" }],
"get": {
"operationId": "operationOverride",
"servers": [{ "url": "https://operation.example.com" }],
"responses": { "200": { "description": "OK" } }
}
},
"/operation-skip-path": {
"get": {
"operationId": "operationSkipPath",
"servers": [{ "url": "https://operation.example.com" }],
"responses": { "200": { "description": "OK" } }
}
},
"/no-servers": {
"get": {
"operationId": "noServers",
"responses": { "200": { "description": "OK" } }
}
}
}
}
@@ -1,8 +1,17 @@
import { describe, expect, it } from 'bun:test';
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
import serverPrecedenceSpec from './fixtures/spec-server-precedence.json';
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
async function loadFixture(spec: object) {
const { filesystem } = await parseOpenAPI({
value: JSON.stringify(spec),
rootURL: 'memory://spec.json',
});
return filesystem;
}
async function fetchFilesystem(url: string) {
const response = await fetch(url);
const text = await response.text();
@@ -174,4 +183,56 @@ describe('#resolveOpenAPIOperation', () => {
},
});
});
describe('server precedence', () => {
it('should use root-level servers when no path or operation servers are defined', async () => {
const filesystem = await loadFixture(serverPrecedenceSpec);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'get',
path: '/root-only',
});
expect(resolved?.servers).toEqual([{ url: 'https://root.example.com' }]);
});
it('should use path-level servers over root-level servers', async () => {
const filesystem = await loadFixture(serverPrecedenceSpec);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'get',
path: '/path-override',
});
expect(resolved?.servers).toEqual([{ url: 'https://path.example.com' }]);
});
it('should use operation-level servers over path and root-level servers', async () => {
const filesystem = await loadFixture(serverPrecedenceSpec);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'get',
path: '/operation-override',
});
expect(resolved?.servers).toEqual([{ url: 'https://operation.example.com' }]);
});
it('should use operation-level servers over root-level when no path servers exist', async () => {
const filesystem = await loadFixture(serverPrecedenceSpec);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'get',
path: '/operation-skip-path',
});
expect(resolved?.servers).toEqual([{ url: 'https://operation.example.com' }]);
});
it('should fallback to root-level servers for endpoints without overrides', async () => {
const filesystem = await loadFixture(serverPrecedenceSpec);
const resolved = await resolveOpenAPIOperation(filesystem, {
method: 'get',
path: '/no-servers',
});
expect(resolved?.servers).toEqual([{ url: 'https://root.example.com' }]);
});
});
});
@@ -37,7 +37,7 @@ export async function resolveOpenAPIOperation(
};
}
const servers = 'servers' in schema ? (schema.servers ?? []) : [];
const servers = getServers(schema, path, operation);
const schemaSecurity = Array.isArray(schema.security)
? schema.security
: schema.security
@@ -112,6 +112,27 @@ function getPathObjectParameter(
return null;
}
/**
* Resolve servers for an operation following OpenAPI precedence rules.
* Per the spec, only the lowest-level servers array is used: operation > path > root.
*/
function getServers(
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
path: string,
operation: OpenAPIV3.OperationObject
): OpenAPIV3.ServerObject[] {
if ('servers' in operation && operation.servers) {
return operation.servers;
}
const pathObject = getPathObject(schema, path);
if (pathObject && 'servers' in pathObject && pathObject.servers) {
return pathObject.servers;
}
return 'servers' in schema ? (schema.servers ?? []) : [];
}
/**
* Get an operation by its path and method.
*/