|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- # Follow-up: SPA routing on Apache (`/paymentPage/callback` and deep links)
-
- Payment gateway redirects and bookmarked URLs hit paths like
- `/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=...`. In Chrome
- DevTools, the **document** request often shows **404 Not Found**, while
- the payment callback logic still runs (console logs from
- `PaymentCallback.js`, success/cancel/fail UI).
-
- This document explains why that happens, how it relates to the current
- Apache vhost (reverse proxy to `192.168.8.x:8080`), and what to change for
- a proper SPA setup (document **200** + unchanged browser URL and query
- string).
-
- Related docs:
-
- - [csp-apache.conf.md](./csp-apache.conf.md) — CSP headers on Apache
- - [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — noted document `status-code: 404` on SPA shell (including `/paymentPage/callback`)
- - [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md) — `/api/*` must not be rewritten to `index.html`
-
- ---
-
- ## Symptom
-
- | Observation | Typical meaning |
- |---|---|
- | `GET /paymentPage/callback?...` → **404** | Origin (or Apache) has no file at that path |
- | Response `Server: GLD Server B`, `Content-Type: text/html` | Apache / front tier answered |
- | Console still logs transaction data | React loaded (often via `index.html` body with wrong status) |
- | Network shows red 404 on the **document** | Cosmetic for users if the shell loads; bad for monitoring, SEO, and some gateways |
-
- Example URL (card payment return from GCIS):
-
- ```text
- https://pnsps.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=...
- ```
-
- ---
-
- ## This is not a missing React route
-
- The client route exists and is wired to the callback page:
-
- | Piece | Location |
- |---|---|
- | Route `paymentPage/callback` | `src/routes/PublicUserRoutes.js` |
- | Component | `src/pages/Payment/PaymentCallback.js` |
- | Card `returnurl` | `src/pages/Payment/Card/index.js` (`/paymentPage/callback`) |
-
- React Router only runs **after** the browser loads the SPA shell
- (`index.html` + JS). The **first** request is a full-page **GET** from
- the payment gateway (or user bookmark). Apache / Tomcat must serve the
- shell for that path; the router never sees a request that never delivered
- HTML.
-
- Real callback work is **POST** to the backend after the page loads:
-
- | Step | API (via `ApiPathConst.js`) |
- |---|---|
- | 1 | `POST /api/payment/api/updatepayment` (`UPDATE_PAYMENT_API`) |
- | 2 | `POST /api/payment/callback/status` (`PAYMENT_CALLBACK_STATUS_API`) |
-
- Backend (`PNSPS-backend`): `PaymentController` — `/api/updatepayment`,
- `/callback/status` under the payment module (full path includes
- `server.servlet.contextPath: /api` and controller prefix).
-
- ---
-
- ## Current Apache pattern (DEV/UAT example)
-
- Typical vhost (`httpd-vhosts.conf`) for `pnspsdev.gld.gov.hk`:
-
- ```apache
- ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
- ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
- ProxyPass /api/ http://192.168.8.6:8080/api/
- ProxyPass / http://192.168.8.6:8080/
- ProxyPassReverse / http://192.168.8.6:8080/
- ```
-
- With only this:
-
- 1. `GET /paymentPage/callback` is forwarded to `192.168.8.6:8080` as the
- same path.
- 2. There is no file at `/paymentPage/callback` on the origin.
- 3. The origin returns **404** (sometimes still returning `index.html` in
- the body — “SPA shell 404”).
- 4. React may boot, read `window.location.search`, and complete payment.
-
- So: **logic can work while the document status stays 404.**
-
- Spring Boot in this repo is **API-only** (`contextPath: /api`, static
- resource mappings disabled in `application.yml`). The React build is
- served separately (commonly Tomcat or static root on `:8080`). SPA
- fallback must be configured on **Apache** and/or that static tier — not
- in Spring MVC alone.
-
- ---
-
- ## Design rules (all options)
-
- 1. **`/api/`** — Always proxy to the backend. Never rewrite API paths to
- `index.html`. (See [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md).)
- 2. **Other proxied apps** — Keep more specific `ProxyPass` prefixes
- (`/paymentrecon-dept_web/`, `/paymentrecon-portal_web/`, etc.) **before**
- the catch-all `/`.
- 3. **Deep links** — For `GET` requests that are not files and not API,
- serve `index.html` with HTTP **200**.
- 4. **Query string** — Payment callback needs `?TRANSACTION_ID=...&RECEIPT_TOKEN=...`
- in the **browser** URL. Use internal rewrite to `index.html`, not an
- external redirect that drops params.
- 5. **Avoid** `ErrorDocument 404 /index.html` as the primary SPA strategy:
- body may be the app while status remains **404** (same noise as today).
-
- ---
-
- ## Option A (recommended): Apache serves the React build
-
- Deploy `npm run build` output to the Apache host. Proxy only API (and
- other backends). Remove catch-all `ProxyPass /` to the app server for
- the main site.
-
- ```apache
- <VirtualHost *:443>
- ServerName pnspsdev.gld.gov.hk
- # ... SSLEngine, certificates ...
-
- DocumentRoot /var/www/pnsps/build
-
- <Directory /var/www/pnsps/build>
- Options -Indexes +FollowSymLinks
- AllowOverride None
- Require all granted
- # Apache 2.4.16+ — unknown paths → index.html, status 200
- FallbackResource /index.html
- </Directory>
-
- ProxyPreserveHost On
-
- ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
- ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
-
- ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
- ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
-
- ProxyPass /api/ http://192.168.8.6:8080/api/
- ProxyPassReverse /api/ http://192.168.8.6:8080/api/
-
- # Do not ProxyPass / to 8080 for the main SPA when using DocumentRoot
- </VirtualHost>
- ```
-
- **Deploy:** After each frontend release, sync `build/` to
- `/var/www/pnsps/build` (or your chosen path) on the Apache server.
-
- **Pros:** Clear split — static on Apache, API on Spring. Easy to reason
- about 200 vs 404.
-
- **Cons:** Requires a deploy path for static files on the Apache tier.
-
- ---
-
- ## Option B: Keep `ProxyPass /` — rewrite deep links to `index.html` before proxy
-
- Use when the React app must stay on `192.168.8.6:8080` and you only
- control Apache. Rewrite eligible **GET** requests to `/index.html` on the
- upstream; browser URL and query string stay unchanged (`[PT,QSA]`).
-
- Add **inside the same vhost**, after enabling `RewriteEngine` (you may
- already have TRACE/TRACK rules):
-
- ```apache
- RewriteEngine on
-
- # Existing: deny TRACE / TRACK
- RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
- RewriteRule .* - [F]
-
- # Do not SPA-rewrite API or other proxied applications
- RewriteCond %{REQUEST_URI} ^/api/ [OR]
- RewriteCond %{REQUEST_URI} ^/paymentrecon-dept_web/ [OR]
- RewriteCond %{REQUEST_URI} ^/paymentrecon-portal_web/
- RewriteRule ^ - [L]
-
- # Pass through static assets (Create React App uses /static/...)
- RewriteCond %{REQUEST_URI} ^/static/ [OR]
- RewriteCond %{REQUEST_URI} \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|map|json|txt|webmanifest)$ [NC]
- RewriteRule ^ - [L]
-
- # SPA fallback: upstream receives GET /index.html; browser keeps /paymentPage/callback?...
- RewriteCond %{REQUEST_METHOD} GET
- RewriteCond %{REQUEST_URI} !^/index\.html$
- RewriteRule ^ /index.html [PT,QSA,L]
-
- # Existing ProxyPass (specific paths first)
- ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
- ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
-
- ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
- ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
-
- ProxyPass /api/ http://192.168.8.6:8080/api/
- ProxyPassReverse /api/ http://192.168.8.6:8080/api/
-
- ProxyPass / http://192.168.8.6:8080/
- ProxyPassReverse / http://192.168.8.6:8080/
- ```
-
- If rewrite and proxy order behaves differently on your Apache version,
- try explicit proxy on the last rule:
-
- ```apache
- RewriteRule ^ /index.html [P,QSA,L]
- ```
-
- (requires `mod_proxy` and correct module load order).
-
- **Pros:** No change to where the WAR/static tree lives on 8080.
-
- **Cons:** Rewrite + `ProxyPass` interaction must be tested per environment;
- static file detection is by URL pattern, not upstream filesystem.
-
- ---
-
- ## Option C: SPA fallback on Tomcat (origin `:8080`)
-
- If Apache cannot change, configure the tier that serves `/` on port 8080
- (Tomcat `RewriteValve`, filter, or error handling) so unknown **GET**
- paths return `index.html` with **200**. Same idea as Options A/B; only the
- layer moves.
-
- ---
-
- ## Verification checklist
-
- Replace host with DEV/UAT/PROD as appropriate (`pnspsdev.gld.gov.hk`,
- `pnsps.gld.gov.hk`, etc.).
-
- ### 1. Payment callback document (main fix)
-
- ```bash
- curl -sS -D - -o /dev/null \
- "https://pnspsdev.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=test&RECEIPT_TOKEN=test"
- ```
-
- | Expected after fix | Before fix (typical) |
- |---|---|
- | **200**, `Content-Type: text/html` | **404**, HTML body may still be SPA |
-
- ### 2. API still proxied (not rewritten to index)
-
- ```bash
- curl -sS -D - -o /dev/null -X POST \
- "https://pnspsdev.gld.gov.hk/api/payment/callback/status" \
- -H "Content-Type: application/json" \
- -d '{}'
- ```
-
- Expect a **JSON** error from Spring (4xx), **not** `index.html` HTML.
- Status is not 404 from a missing static file.
-
- ### 3. Static assets
-
- ```bash
- curl -sS -D - -o /dev/null \
- "https://pnspsdev.gld.gov.hk/static/js/main.<hash>.chunk.js"
- ```
-
- Use a real hash from the deployed `index.html`. Expect **200**.
-
- ### 4. Other client routes
-
- ```bash
- curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/login"
- curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/dashboard"
- ```
-
- Expect **200** HTML shell when logged-out routes are publicly reachable
- (may redirect in browser; curl shows initial response).
-
- ### 5. Browser — card payment end-to-end
-
- 1. Start card payment; complete or cancel on GCIS gateway.
- 2. DevTools → Network: document for `/paymentPage/callback?...` → **200**.
- 3. Network: `POST .../api/payment/api/updatepayment` and
- `POST .../api/payment/callback/status` → success responses per business rules.
- 4. UI: success, cancel, or fail page from `PaymentCallback.js`.
-
- ### 6. Apache reload
-
- ```bash
- sudo apachectl configtest
- sudo systemctl reload apache2
- ```
-
- Use `httpd` / `apachectl` as on your GLD hosts.
-
- ---
-
- ## Application behaviour (after shell loads)
-
- ### Auth gate
-
- `paymentPage/callback` is under `PublicUserRoutes`, which is registered
- only when the user is logged in as IND or ORG (`src/routes/index.js`).
- If the session expires on the gateway, the user may hit `/login` instead
- of the callback page. That is separate from the document 404; test with a
- fresh login if callback APIs never fire.
-
- ### FPS vs card
-
- | Flow | Return path | Route file |
- |---|---|---|
- | Card | `/paymentPage/callback` | `PublicUserRoutes.js` |
- | FPS (PASG) | `/paymentPage/fps/fpscallback` | `PublicUserRoutes.js` (+ test route in `LoginRoutes.js`) |
-
- This follow-up focuses on **card** `returnurl` to `/paymentPage/callback`.
-
- ### Code fragility (optional app fix, not Apache)
-
- `PaymentCallback.js` uses
- `params.get("RECEIPT_TOKEN").replace("%3D", "=")` without a null check.
- Missing `RECEIPT_TOKEN` throws before API calls. Harden separately if
- gateways ever omit the param.
-
- ---
-
- ## What not to do
-
- | Idea | Why |
- |---|---|
- | Add only a React route | Does not fix the first GET to Apache/Tomcat |
- | `Redirect` callback URL to `/` | Loses query params unless carefully preserved |
- | Rewrite `/api/` to `index.html` | Breaks payment POSTs, logout, login, etc. |
- | Rely on 404 + `index.html` body | Payment may work; monitoring and CSP reports stay noisy |
- | Change CSP for this | Not a CSP block; document status is HTTP routing |
-
- ---
-
- ## Decision summary
-
- | Option | Where SPA fallback lives | Remove `ProxyPass /`? |
- |---|---|---|
- | A | Apache `DocumentRoot` + `FallbackResource` | Yes (for main site) |
- | B | Apache `RewriteRule` → `/index.html` + existing proxy | No |
- | C | Tomcat / origin on 8080 | No Apache change |
-
- **Recommended:** Option A if operations can publish `build/` to Apache;
- Option B if the static tree must remain on `192.168.8.6:8080`.
-
- After implementation, `/paymentPage/callback` should show **200** on the
- document request while behaviour of `PaymentCallback.js` and payment APIs
- stays the same.
-
- ---
-
- ## Code map (quick reference)
-
- | Piece | Location |
- |---|---|
- | Callback route | `PNSPS-frontend-MaterialUI/src/routes/PublicUserRoutes.js` |
- | Callback UI + API calls | `PNSPS-frontend-MaterialUI/src/pages/Payment/PaymentCallback.js` |
- | Card return URL | `PNSPS-frontend-MaterialUI/src/pages/Payment/Card/index.js` |
- | API constants | `PNSPS-frontend-MaterialUI/src/utils/ApiPathConst.js` |
- | Route auth wiring | `PNSPS-frontend-MaterialUI/src/routes/index.js` |
- | Payment APIs | `PNSPS-backend/.../payment/web/PaymentController.java` |
- | API context path | `PNSPS-backend/src/main/resources/application.yml` (`/api`) |
-
- ---
-
- ## Change log (doc)
-
- | Date | Note |
- |---|---|
- | 2026-10-14 | Initial follow-up from payment callback 404 investigation and DEV vhost review |
|