-
Notifications
You must be signed in to change notification settings - Fork 13.8k
chore(api): experimental REST API namespace (/api/experimental) #41116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3ad7e13
feat(api): add experimental API instance
sampaiodiego cb4c262
feat(api): mount experimental router and metrics
sampaiodiego 413ffd4
feat(api): add experimental unstable-signal middleware
sampaiodiego fdabf2b
feat(rest-typings): add opt-in ExperimentalEndpoints
sampaiodiego ea979f1
chore(api): add experimental guardrails and docs
sampaiodiego b2984fc
apply rate limiters
sampaiodiego 712cfc5
fix metrics getting doubled
sampaiodiego fb5fe82
omit addRoute from experimental
sampaiodiego 1001f4d
fix experimental headers
sampaiodiego 728e639
fix metrics again
sampaiodiego 0796de1
doc: fix doc items
sampaiodiego 1e13370
remove enforcement for experimental api
sampaiodiego File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
apps/meteor/server/api/v1/middlewares/experimental.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { Router } from '@rocket.chat/http-router'; | ||
| import Ajv from 'ajv'; | ||
| import express from 'express'; | ||
| import request from 'supertest'; | ||
|
|
||
| import { cors } from './cors'; | ||
| import { experimentalWarningMiddleware } from './experimental'; | ||
| import { CachedSettings } from '../../../settings/CachedSettings'; | ||
|
|
||
| const WARNING_HEADER = '299 - "experimental: endpoint is unstable and may change without notice"'; | ||
|
|
||
| const buildApp = ({ corsEnabled }: { corsEnabled: boolean }) => { | ||
| const ajv = new Ajv(); | ||
| const settings = new CachedSettings(); | ||
| settings.set({ _id: 'API_Enable_CORS', value: corsEnabled } as any); | ||
| settings.set({ _id: 'API_CORS_Origin', value: 'https://allowed.example' } as any); | ||
|
|
||
| const route = (router: Router<any, any, any>) => | ||
| router.get('/test', { response: { 200: ajv.compile({ type: 'object' }) } }, async () => ({ | ||
| statusCode: 200 as const, | ||
| body: {}, | ||
| })); | ||
|
|
||
| const api = new Router('/api') | ||
| .use(experimentalWarningMiddleware({ basePathRegex: new RegExp(/^\/api\/experimental(\/|$)/) })) | ||
| .use(cors(settings)) | ||
| .use(route(new Router('/v1'))) | ||
| .use(route(new Router('/experimental'))); | ||
|
|
||
| const app = express(); | ||
| app.use(api.router); | ||
| return app; | ||
| }; | ||
|
|
||
| const preflight = (app: express.Express, path: string, origin: string) => | ||
| request(app).options(path).set('Origin', origin).set('Access-Control-Request-Method', 'GET'); | ||
|
|
||
| describe('Experimental middleware', () => { | ||
| it('should stamp the unstable signal headers on experimental responses', async () => { | ||
| const res = await request(buildApp({ corsEnabled: true })).get('/api/experimental/test'); | ||
|
|
||
| expect(res.statusCode).toBe(200); | ||
| expect(res.headers['x-experimental']).toBe('true'); | ||
| expect(res.headers.warning).toBe(WARNING_HEADER); | ||
| }); | ||
|
|
||
| it('should not stamp responses from other versions', async () => { | ||
| const res = await request(buildApp({ corsEnabled: true })).get('/api/v1/test'); | ||
|
|
||
| expect(res.statusCode).toBe(200); | ||
| expect(res.headers['x-experimental']).toBeUndefined(); | ||
| expect(res.headers.warning).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should stamp 404s for unmatched experimental paths', async () => { | ||
| const res = await request(buildApp({ corsEnabled: true })).get('/api/experimental/nope'); | ||
|
|
||
| expect(res.statusCode).toBe(404); | ||
| expect(res.headers['x-experimental']).toBe('true'); | ||
| }); | ||
|
|
||
| // cors answers rejected preflights without calling next(), so these only carry the headers | ||
| // while the middleware stays registered ahead of it | ||
| it('should stamp preflight rejections when CORS is disabled', async () => { | ||
| const res = await preflight(buildApp({ corsEnabled: false }), '/api/experimental/test', 'https://allowed.example'); | ||
|
|
||
| expect(res.statusCode).toBe(405); | ||
| expect(res.headers['x-experimental']).toBe('true'); | ||
| expect(res.headers.warning).toBe(WARNING_HEADER); | ||
| }); | ||
|
|
||
| it('should stamp preflight rejections from disallowed origins', async () => { | ||
| const res = await preflight(buildApp({ corsEnabled: true }), '/api/experimental/test', 'https://evil.example'); | ||
|
|
||
| expect(res.statusCode).toBe(403); | ||
| expect(res.headers['x-experimental']).toBe('true'); | ||
| expect(res.headers.warning).toBe(WARNING_HEADER); | ||
| }); | ||
|
|
||
| it('should not stamp preflight rejections from other versions', async () => { | ||
| const res = await preflight(buildApp({ corsEnabled: true }), '/api/v1/test', 'https://evil.example'); | ||
|
|
||
| expect(res.statusCode).toBe(403); | ||
| expect(res.headers['x-experimental']).toBeUndefined(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import type { MiddlewareHandler } from 'hono'; | ||
|
|
||
| // `x-experimental` is the supported programmatic signal. `Warning: 299` is emitted for | ||
| // legacy tooling only — warn code 299 came from RFC 7234, which RFC 9111 has obsoleted | ||
| // along with the `Warning` header itself. | ||
| const WARNING_HEADER = '299 - "experimental: endpoint is unstable and may change without notice"'; | ||
|
|
||
| /** | ||
| * Stamps every experimental response with the unstable signal headers. | ||
| * | ||
| * Registered on the shared `/api` mount ahead of `cors`, and scoped by path rather than by | ||
| * router: `cors` answers rejected preflights with 403/405 without calling `next()`, so a | ||
| * middleware living on `API.experimental.router` would never run for those responses. | ||
| * | ||
| * The headers are set on `c.res.headers` before the downstream handlers run; Hono merges them | ||
| * into whatever response is produced later, so 404s and CORS rejections are covered too. | ||
| */ | ||
| export const experimentalWarningMiddleware = | ||
| ({ basePathRegex }: { basePathRegex: RegExp }): MiddlewareHandler => | ||
| async (c, next) => { | ||
| if (!basePathRegex.test(c.req.path)) { | ||
| return next(); | ||
| } | ||
|
|
||
| c.res.headers.set('x-experimental', 'true'); | ||
| c.res.headers.set('Warning', WARNING_HEADER); | ||
|
|
||
| await next(); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.