From f9c6ff83306eef034c376afdaf4132a8541ad36e Mon Sep 17 00:00:00 2001 From: Wukong Sun Date: Wed, 29 Apr 2026 15:18:52 +0800 Subject: [PATCH] feat(api): fw-7456 add CORS support for JSON API endpoints Add Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers to all JSON API responses, and handle OPTIONS preflight requests with 204 No Content. This ensures cross-origin requests from browsers work consistently regardless of CDN cache state. --- src/jsons/health.nim | 8 +++++++- src/nitter.nim | 14 +++++++++++++- src/routes/router_utils.nim | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/jsons/health.nim b/src/jsons/health.nim index 2a0ee7dd1..e74fcf4d0 100644 --- a/src/jsons/health.nim +++ b/src/jsons/health.nim @@ -11,5 +11,11 @@ proc createJsonApiHealthRouter*(cfg: Config) = router jsonapi_health: get "/api/health": cond cfg.enableJsonApi - let headers = {"Content-Type": "application/json; charset=utf-8"} + let origin = corsOrigin() + let headers = { + "Content-Type": "application/json; charset=utf-8", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + } resp Http200, headers, """{"message": "OK"}""" diff --git a/src/nitter.nim b/src/nitter.nim index 42ea3359a..557201674 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, strformat, logging +import asyncdispatch, strformat, logging, re from net import Port from htmlgen import a from os import getEnv @@ -71,6 +71,18 @@ settings: reusePort = true routes: + # CORS preflight handler for API routes + options re"/api/.*": + let origin = if request.headers.hasKey("Origin"): request.headers["Origin"] else: "*" + resp Http204, { + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, DNT", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "300" + }, "" + get "/": resp renderMain(renderSearch(), request, cfg, themePrefs()) diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim index cb2ead0a2..c4e5337d3 100644 --- a/src/routes/router_utils.nim +++ b/src/routes/router_utils.nim @@ -43,25 +43,51 @@ template getCursor*(req: Request): string = proc getNames*(name: string): seq[string] = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) +template corsOrigin*(): string {.dirty.} = + if request.headers.hasKey("Origin"): request.headers["Origin"] else: "*" + template respJson*(node: JsonNode) = - resp $node, "application/json" + let origin = corsOrigin() + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $node template respJsonSuccess*(data: JsonNode) = + let origin = corsOrigin() let successResponse = %*{ "code": 0, "data": data } - resp $successResponse, "application/json" + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $successResponse template respJsonError*(message: string, errorType: string = "", httpCode: HttpCode = Http200) = + let origin = corsOrigin() var errorResponse = %*{ "code": -1, "error": message } if errorType.len > 0: errorResponse["error_type"] = %errorType - resp httpCode, $errorResponse, "application/json" + resp httpCode, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $errorResponse template respJsonNull*() = - let nullResponse = newJNull() - resp $nullResponse, "application/json" + let origin = corsOrigin() + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $newJNull()