Skip to content

Commit e841d45

Browse files
Handle internal matchmaking routes in gateway
1 parent 680ab6e commit e841d45

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

gateway/gateway-service.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,17 @@ function buildForwardHeaders(req, user, extraHeaders = {}) {
394394
return headers;
395395
}
396396

397+
function buildInternalForwardHeaders(req, user, extraHeaders = {}) {
398+
const headers = buildForwardHeaders(req, user, extraHeaders);
399+
const forwardedUserId = normalizeOptionalString(req.get('x-user-id'));
400+
401+
if (!headers['x-user-id'] && forwardedUserId) {
402+
headers['x-user-id'] = forwardedUserId;
403+
}
404+
405+
return headers;
406+
}
407+
397408
function pickPlayBotId(body) {
398409
const explicitBotId = normalizeOptionalString(body?.bot_id);
399410
if (explicitBotId) {
@@ -871,6 +882,67 @@ function createExternalApiRouter({ env = process.env, fetchImpl = globalThis.fet
871882
return router;
872883
}
873884

885+
function createInternalApiRouter({ serviceUrls, fetchImpl }) {
886+
const router = express.Router();
887+
888+
router.post('/v1/matchmaking/enqueue', express.json(), asyncRoute(async (req, res) => {
889+
const user = await getOptionalUser(req, serviceUrls, fetchImpl);
890+
const result = await fetchJson(
891+
fetchImpl,
892+
'gamey service',
893+
`${serviceUrls.gamey}/v1/matchmaking/enqueue`,
894+
buildJsonInit('POST', req.body, buildInternalForwardHeaders(req, user, {
895+
'Content-Type': 'application/json',
896+
})),
897+
);
898+
899+
sendPayload(res, result.status, result.payload);
900+
}));
901+
902+
router.get('/v1/matchmaking/tickets/:ticketId', asyncRoute(async (req, res) => {
903+
const result = await fetchJson(
904+
fetchImpl,
905+
'gamey service',
906+
`${serviceUrls.gamey}/v1/matchmaking/tickets/${encodeURIComponent(req.params.ticketId)}`,
907+
{ method: 'GET' },
908+
);
909+
910+
sendPayload(res, result.status, result.payload);
911+
}));
912+
913+
router.post('/v1/matchmaking/tickets/:ticketId/cancel', asyncRoute(async (req, res) => {
914+
const result = await fetchJson(
915+
fetchImpl,
916+
'gamey service',
917+
`${serviceUrls.gamey}/v1/matchmaking/tickets/${encodeURIComponent(req.params.ticketId)}/cancel`,
918+
{ method: 'POST' },
919+
);
920+
921+
sendPayload(res, result.status, result.payload);
922+
}));
923+
924+
router.use((error, _req, res, _next) => {
925+
if (res.headersSent) {
926+
return;
927+
}
928+
929+
if (error instanceof HttpResponseError) {
930+
sendPayload(res, error.status, error.payload ?? { message: error.message });
931+
return;
932+
}
933+
934+
if (error instanceof UpstreamConnectionError) {
935+
res.status(502).json({ message: `Bad Gateway: ${error.serviceName} unavailable` });
936+
return;
937+
}
938+
939+
console.error(error);
940+
res.status(500).json({ message: error.message || 'Internal server error' });
941+
});
942+
943+
return router;
944+
}
945+
874946
function createApp({
875947
env = process.env,
876948
proxyFactory = createProxyMiddleware,
@@ -904,6 +976,7 @@ function createApp({
904976
}
905977
}));
906978

979+
app.use('/api', createInternalApiRouter({ serviceUrls, fetchImpl }));
907980

908981
for (const route of proxyRoutes) {
909982
if (route.mountPath === '/api') {

gateway/tests/services/gateway-service-routes.test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,76 @@ test('POST /external/v1/games works without auth token for anonymous play', asyn
822822
});
823823
});
824824
});
825+
826+
// ---------------------------------------------------------------------------
827+
// POST /api/v1/matchmaking/enqueue
828+
// ---------------------------------------------------------------------------
829+
830+
test('POST /api/v1/matchmaking/enqueue forwards JSON and session user id to gamey', async () => {
831+
await withJsonServer(async (req, res, body) => {
832+
assert.equal(req.method, 'POST');
833+
assert.equal(req.url, '/v1/matchmaking/enqueue');
834+
assert.equal(req.headers['content-type'], 'application/json');
835+
assert.equal(req.headers['x-user-id'], 'adri');
836+
assert.deepEqual(body, { size: 7 });
837+
838+
jsonResponse(res, 200, {
839+
api_version: 'v1',
840+
ticket_id: 'ticket-1',
841+
status: 'waiting',
842+
poll_after_ms: 1000,
843+
position: 1,
844+
game_id: null,
845+
player_id: null,
846+
player_token: null,
847+
});
848+
}, async (gameyUrl) => {
849+
const { app } = createApp({
850+
proxyFactory: noopProxyFactory,
851+
env: { GAMEY_SERVICE_URL: gameyUrl },
852+
});
853+
854+
await withServer(app, async (baseUrl) => {
855+
const response = await fetch(`${baseUrl}/api/v1/matchmaking/enqueue`, {
856+
method: 'POST',
857+
headers: {
858+
'Content-Type': 'application/json',
859+
'x-user-id': 'adri',
860+
},
861+
body: JSON.stringify({ size: 7 }),
862+
});
863+
const body = await response.json();
864+
865+
assert.equal(response.status, 200);
866+
assert.equal(body.ticket_id, 'ticket-1');
867+
assert.equal(body.status, 'waiting');
868+
});
869+
});
870+
});
871+
872+
test('POST /api/v1/matchmaking/enqueue forwards gamey validation errors', async () => {
873+
await withJsonServer(async (_req, res) => {
874+
jsonResponse(res, 400, {
875+
api_version: 'v1',
876+
bot_id: null,
877+
message: 'User already has an active matchmaking ticket: ticket-1',
878+
});
879+
}, async (gameyUrl) => {
880+
const { app } = createApp({
881+
proxyFactory: noopProxyFactory,
882+
env: { GAMEY_SERVICE_URL: gameyUrl },
883+
});
884+
885+
await withServer(app, async (baseUrl) => {
886+
const response = await fetch(`${baseUrl}/api/v1/matchmaking/enqueue`, {
887+
method: 'POST',
888+
headers: { 'Content-Type': 'application/json' },
889+
body: JSON.stringify({ size: 7 }),
890+
});
891+
const body = await response.json();
892+
893+
assert.equal(response.status, 400);
894+
assert.match(body.message, /active matchmaking ticket/);
895+
});
896+
});
897+
});

0 commit comments

Comments
 (0)