connect-src reports for /api/logout (404)PROD CSP reports from 2026-10-13 and 2026-10-14. The browser is on
https://pnsps.gld.gov.hk/login and reports
https://pnsps.gld.gov.hk/api/logout as a connect-src violation with
HTTP 404.
This is not a React route, and it is not a Content-Security-Policy block. The logout API call is reaching something that answers “not found”. Chrome then files a report-only CSP violation for that failed same-origin request.
Related docs:
status-code: 404 on the SPA shell)Example (same shape on every hit in this batch):
| Field | Value | Meaning |
|---|---|---|
document-uri |
https://pnsps.gld.gov.hk/login |
The page that was loaded when the report was sent |
referrer |
usually https://pnsps.gld.gov.hk/dashboard; one hit from https://epaygateway2.gcis.gov.hk/ |
Where the user was before /login |
violated-directive |
connect-src |
Chrome classified the failed request under connect-src |
blocked-uri |
https://pnsps.gld.gov.hk/api/logout |
The request that failed |
status-code |
404 |
The server answered. A real CSP block is status 0 |
disposition |
report |
Report-Only. The policy did not enforce a block |
| Policy | connect-src 'self' |
Same host is already allowed |
Do not add anything to connect-src for this. 'self' already covers
https://pnsps.gld.gov.hk.
React Router only runs after an HTML document has loaded, and only for the
address bar (/login, /dashboard, and the other paths in
src/routes/). It never sees axios / fetch calls.
/api/logout is an API call, not a page:
/dashboard (or returns from the payment gateway)./api/logout, then moves the browser to /login.Frontend call (PNSPS-frontend-MaterialUI):
src/utils/ApiPathConst.js — LOGOUT = "/logout"src/auth/utils.js — production apiPath is /apisrc/auth/index.js — handleLogoutFunction does
axios.post(\${apiPath}${LOGOUT}`, { refreshToken }), which isPOST /api/logout`navigate('/login'). The axios 401 path also does
window.location.reload()/login is already a React route (src/routes/LoginRoutes.js, path
login). The document in the report loaded. The 404 is the POST, not the
login page.
Adding a React route for /api/logout or /logout does not receive this
POST. The browser sends it to Apache before React runs.
PNSPS-backend:
src/main/resources/application.yml — server.servlet.contextPath: /apiJwtAuthenticationController — @PostMapping("/logout"), response
204 No ContentSecurityConfig — LOGOUT_URL = "/logout" is on URL_WHITELIST
(permitAll), so an expired session can still call itRefreshTokenService.revokePresented — unknown or bad tokens return
without an error. A request that hits this method is still 204So a correct POST /api/logout is 204, including a dummy refresh token.
404 means the request never entered that method.
Two rules, in this order. /api must be proxied to Spring. Page paths that
are not real files must be served as index.html with status 200.
/api goes todayOn the PROD Apache host, find the vhost for pnsps.gld.gov.hk and note:
ProxyPass / ProxyPassReverse for /apiRewriteRule that sends unknown paths to index.htmlErrorDocument 404 or FallbackResourceErrorDocument 404 /index.html is the wrong tool for a React app. The body
can be the SPA while the status stays 404. That is the “SPA shell 404”
called out in the 2026-08-03 and 2026-09-22 reviews. It also makes a missed
API path look like a client route.
/api on the backend/api must be handled before any SPA rewrite, and must not be rewritten to
index.html.
ProxyPreserveHost On
ProxyPass /api http://127.0.0.1:8090/api
ProxyPassReverse /api http://127.0.0.1:8090/api
Use the real upstream (host and port) already used for
/api/csp-report and /api/login. Do not invent a new port if PROD is
not on 8090.
If a rewrite engine is also on, stop rewriting /api:
RewriteEngine On
RewriteRule ^/api/ - [L]
If /api/logout is rewritten to index.html:
index.htmlApply this only after the /api rule. It covers /login, /dashboard,
and the other client paths. Real files (/static/js/..., css, images) stay
as files.
RewriteEngine On
# API is the backend, not a React route. Must stay above the fallback.
RewriteRule ^/api/ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
Remove ErrorDocument 404 /index.html for this vhost if it is what currently
loads the SPA. FallbackResource /index.html is acceptable only when
/api is excluded and the fallback response is 200.
The 204 handler has to be in the build that PROD is running. A proxy to an
old jar that has no POST /logout still returns 404 from Spring.
sudo apachectl configtest
sudo systemctl reload apache2
Use httpd instead of apache2 if that is the service name on this host.
From a machine that can reach PROD:
curl -sS -D - -o /dev/null \
-X POST "https://pnsps.gld.gov.hk/api/logout" \
-H "Content-Type: application/json" \
-d '{"refreshToken":"not-a-real-token"}'
| Status | Meaning | Next action |
|---|---|---|
| 204 | Handler ran. Bad token is ignored on purpose | Apache and the deployed jar are fine. Go to step 6 |
| 404 | Still not the logout method | Response body: Spring JSON means the proxy works but this build has no mapping. HTML (index.html or an Apache error page) means the SPA/error fallback caught /api/logout. Fix step 2, and deploy the backend if the body is Spring’s 404 |
| 401 | Request reached Spring Security and was not on the whitelist | Deploy the build where LOGOUT_URL is in URL_WHITELIST |
| 405 | Path exists, method does not match | Something sent GET. The app sends POST. Check for a redirect that turned POST into GET |
| 502 / 503 | Proxy target down or wrong | Fix the ProxyPass upstream |
Also check a real page, not only the API:
curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/login"
curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/dashboard"
Both should be 200 and Content-Type: text/html (the SPA shell). A
404 here means step 3 is not in effect.
/dashboard.POST /api/logout → 204, then the document request for
/login → 200.src/auth/index.js posts logout, then navigate('/login') and
window.location.reload()). Same 204, then /login 200.The payment-gateway referrer in one report is the same logout POST. The
user landed on /login after epaygateway2.gcis.gov.hk; it is not a
separate route to allowlist.
After the 204s are confirmed, new connect-src hits for
blocked-uri https://pnsps.gld.gov.hk/api/logout should stop. Reports
already stored are historical.
Leave the CSP header as in csp-apache.conf.md.
Do not add origins, and do not drop connect-src 'self'.
| Idea | Why it does not fix this |
|---|---|
New React route for /logout or /api/logout |
The POST never goes through the router. LogoutPage.js exists and is not mounted; mounting it would not see axios.post |
Rewrite /api/* to index.html |
Logout never revokes the refresh token |
Allow /api/logout in CSP |
'self' already allows it. Status 404 shows the request was sent |
| Turn off Report-Only to “hide” it | Hides the symptom. The POST would still 404 |
| Piece | Location |
|---|---|
| Logout POST | PNSPS-frontend-MaterialUI/src/auth/index.js (handleLogoutFunction) |
401 → logout → /login → reload |
same file, axios response interceptor |
| Header logout | src/layout/MainLayout/Header/index.js (handleLogout) |
| API path | src/utils/ApiPathConst.js, src/auth/utils.js |
| Login route | src/routes/LoginRoutes.js (path: 'login') |
POST /logout → 204 |
PNSPS-backend JwtAuthenticationController |
| Whitelist | SecurityConfig.LOGOUT_URL |
Context path /api |
src/main/resources/application.yml |