Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

spa-apache-routing-follow-up.md 13 KiB

3 veckor sedan
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. # Follow-up: SPA routing on Apache (`/paymentPage/callback` and deep links)
  2. Payment gateway redirects and bookmarked URLs hit paths like
  3. `/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=...`. In Chrome
  4. DevTools, the **document** request often shows **404 Not Found**, while
  5. the payment callback logic still runs (console logs from
  6. `PaymentCallback.js`, success/cancel/fail UI).
  7. This document explains why that happens, how it relates to the current
  8. Apache vhost (reverse proxy to `192.168.8.x:8080`), and what to change for
  9. a proper SPA setup (document **200** + unchanged browser URL and query
  10. string).
  11. Related docs:
  12. - [csp-apache.conf.md](./csp-apache.conf.md) — CSP headers on Apache
  13. - [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`)
  14. - [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md) — `/api/*` must not be rewritten to `index.html`
  15. ---
  16. ## Symptom
  17. | Observation | Typical meaning |
  18. |---|---|
  19. | `GET /paymentPage/callback?...` → **404** | Origin (or Apache) has no file at that path |
  20. | Response `Server: GLD Server B`, `Content-Type: text/html` | Apache / front tier answered |
  21. | Console still logs transaction data | React loaded (often via `index.html` body with wrong status) |
  22. | Network shows red 404 on the **document** | Cosmetic for users if the shell loads; bad for monitoring, SEO, and some gateways |
  23. Example URL (card payment return from GCIS):
  24. ```text
  25. https://pnsps.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=...&RECEIPT_TOKEN=...
  26. ```
  27. ---
  28. ## This is not a missing React route
  29. The client route exists and is wired to the callback page:
  30. | Piece | Location |
  31. |---|---|
  32. | Route `paymentPage/callback` | `src/routes/PublicUserRoutes.js` |
  33. | Component | `src/pages/Payment/PaymentCallback.js` |
  34. | Card `returnurl` | `src/pages/Payment/Card/index.js` (`/paymentPage/callback`) |
  35. React Router only runs **after** the browser loads the SPA shell
  36. (`index.html` + JS). The **first** request is a full-page **GET** from
  37. the payment gateway (or user bookmark). Apache / Tomcat must serve the
  38. shell for that path; the router never sees a request that never delivered
  39. HTML.
  40. Real callback work is **POST** to the backend after the page loads:
  41. | Step | API (via `ApiPathConst.js`) |
  42. |---|---|
  43. | 1 | `POST /api/payment/api/updatepayment` (`UPDATE_PAYMENT_API`) |
  44. | 2 | `POST /api/payment/callback/status` (`PAYMENT_CALLBACK_STATUS_API`) |
  45. Backend (`PNSPS-backend`): `PaymentController` — `/api/updatepayment`,
  46. `/callback/status` under the payment module (full path includes
  47. `server.servlet.contextPath: /api` and controller prefix).
  48. ---
  49. ## Current Apache pattern (DEV/UAT example)
  50. Typical vhost (`httpd-vhosts.conf`) for `pnspsdev.gld.gov.hk`:
  51. ```apache
  52. ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
  53. ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
  54. ProxyPass /api/ http://192.168.8.6:8080/api/
  55. ProxyPass / http://192.168.8.6:8080/
  56. ProxyPassReverse / http://192.168.8.6:8080/
  57. ```
  58. With only this:
  59. 1. `GET /paymentPage/callback` is forwarded to `192.168.8.6:8080` as the
  60. same path.
  61. 2. There is no file at `/paymentPage/callback` on the origin.
  62. 3. The origin returns **404** (sometimes still returning `index.html` in
  63. the body — “SPA shell 404”).
  64. 4. React may boot, read `window.location.search`, and complete payment.
  65. So: **logic can work while the document status stays 404.**
  66. Spring Boot in this repo is **API-only** (`contextPath: /api`, static
  67. resource mappings disabled in `application.yml`). The React build is
  68. served separately (commonly Tomcat or static root on `:8080`). SPA
  69. fallback must be configured on **Apache** and/or that static tier — not
  70. in Spring MVC alone.
  71. ---
  72. ## Design rules (all options)
  73. 1. **`/api/`** — Always proxy to the backend. Never rewrite API paths to
  74. `index.html`. (See [csp-api-logout-404-follow-up.md](./csp-api-logout-404-follow-up.md).)
  75. 2. **Other proxied apps** — Keep more specific `ProxyPass` prefixes
  76. (`/paymentrecon-dept_web/`, `/paymentrecon-portal_web/`, etc.) **before**
  77. the catch-all `/`.
  78. 3. **Deep links** — For `GET` requests that are not files and not API,
  79. serve `index.html` with HTTP **200**.
  80. 4. **Query string** — Payment callback needs `?TRANSACTION_ID=...&RECEIPT_TOKEN=...`
  81. in the **browser** URL. Use internal rewrite to `index.html`, not an
  82. external redirect that drops params.
  83. 5. **Avoid** `ErrorDocument 404 /index.html` as the primary SPA strategy:
  84. body may be the app while status remains **404** (same noise as today).
  85. ---
  86. ## Option A (recommended): Apache serves the React build
  87. Deploy `npm run build` output to the Apache host. Proxy only API (and
  88. other backends). Remove catch-all `ProxyPass /` to the app server for
  89. the main site.
  90. ```apache
  91. <VirtualHost *:443>
  92. ServerName pnspsdev.gld.gov.hk
  93. # ... SSLEngine, certificates ...
  94. DocumentRoot /var/www/pnsps/build
  95. <Directory /var/www/pnsps/build>
  96. Options -Indexes +FollowSymLinks
  97. AllowOverride None
  98. Require all granted
  99. # Apache 2.4.16+ — unknown paths → index.html, status 200
  100. FallbackResource /index.html
  101. </Directory>
  102. ProxyPreserveHost On
  103. ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
  104. ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
  105. ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
  106. ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
  107. ProxyPass /api/ http://192.168.8.6:8080/api/
  108. ProxyPassReverse /api/ http://192.168.8.6:8080/api/
  109. # Do not ProxyPass / to 8080 for the main SPA when using DocumentRoot
  110. </VirtualHost>
  111. ```
  112. **Deploy:** After each frontend release, sync `build/` to
  113. `/var/www/pnsps/build` (or your chosen path) on the Apache server.
  114. **Pros:** Clear split — static on Apache, API on Spring. Easy to reason
  115. about 200 vs 404.
  116. **Cons:** Requires a deploy path for static files on the Apache tier.
  117. ---
  118. ## Option B: Keep `ProxyPass /` — rewrite deep links to `index.html` before proxy
  119. Use when the React app must stay on `192.168.8.6:8080` and you only
  120. control Apache. Rewrite eligible **GET** requests to `/index.html` on the
  121. upstream; browser URL and query string stay unchanged (`[PT,QSA]`).
  122. Add **inside the same vhost**, after enabling `RewriteEngine` (you may
  123. already have TRACE/TRACK rules):
  124. ```apache
  125. RewriteEngine on
  126. # Existing: deny TRACE / TRACK
  127. RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
  128. RewriteRule .* - [F]
  129. # Do not SPA-rewrite API or other proxied applications
  130. RewriteCond %{REQUEST_URI} ^/api/ [OR]
  131. RewriteCond %{REQUEST_URI} ^/paymentrecon-dept_web/ [OR]
  132. RewriteCond %{REQUEST_URI} ^/paymentrecon-portal_web/
  133. RewriteRule ^ - [L]
  134. # Pass through static assets (Create React App uses /static/...)
  135. RewriteCond %{REQUEST_URI} ^/static/ [OR]
  136. RewriteCond %{REQUEST_URI} \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|map|json|txt|webmanifest)$ [NC]
  137. RewriteRule ^ - [L]
  138. # SPA fallback: upstream receives GET /index.html; browser keeps /paymentPage/callback?...
  139. RewriteCond %{REQUEST_METHOD} GET
  140. RewriteCond %{REQUEST_URI} !^/index\.html$
  141. RewriteRule ^ /index.html [PT,QSA,L]
  142. # Existing ProxyPass (specific paths first)
  143. ProxyPass /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
  144. ProxyPassReverse /paymentrecon-dept_web/ http://192.168.8.7:18080/paymentrecon-dept_web/
  145. ProxyPass /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
  146. ProxyPassReverse /paymentrecon-portal_web/ http://192.168.8.7:8080/paymentrecon-portal_web/
  147. ProxyPass /api/ http://192.168.8.6:8080/api/
  148. ProxyPassReverse /api/ http://192.168.8.6:8080/api/
  149. ProxyPass / http://192.168.8.6:8080/
  150. ProxyPassReverse / http://192.168.8.6:8080/
  151. ```
  152. If rewrite and proxy order behaves differently on your Apache version,
  153. try explicit proxy on the last rule:
  154. ```apache
  155. RewriteRule ^ /index.html [P,QSA,L]
  156. ```
  157. (requires `mod_proxy` and correct module load order).
  158. **Pros:** No change to where the WAR/static tree lives on 8080.
  159. **Cons:** Rewrite + `ProxyPass` interaction must be tested per environment;
  160. static file detection is by URL pattern, not upstream filesystem.
  161. ---
  162. ## Option C: SPA fallback on Tomcat (origin `:8080`)
  163. If Apache cannot change, configure the tier that serves `/` on port 8080
  164. (Tomcat `RewriteValve`, filter, or error handling) so unknown **GET**
  165. paths return `index.html` with **200**. Same idea as Options A/B; only the
  166. layer moves.
  167. ---
  168. ## Verification checklist
  169. Replace host with DEV/UAT/PROD as appropriate (`pnspsdev.gld.gov.hk`,
  170. `pnsps.gld.gov.hk`, etc.).
  171. ### 1. Payment callback document (main fix)
  172. ```bash
  173. curl -sS -D - -o /dev/null \
  174. "https://pnspsdev.gld.gov.hk/paymentPage/callback?TRANSACTION_ID=test&RECEIPT_TOKEN=test"
  175. ```
  176. | Expected after fix | Before fix (typical) |
  177. |---|---|
  178. | **200**, `Content-Type: text/html` | **404**, HTML body may still be SPA |
  179. ### 2. API still proxied (not rewritten to index)
  180. ```bash
  181. curl -sS -D - -o /dev/null -X POST \
  182. "https://pnspsdev.gld.gov.hk/api/payment/callback/status" \
  183. -H "Content-Type: application/json" \
  184. -d '{}'
  185. ```
  186. Expect a **JSON** error from Spring (4xx), **not** `index.html` HTML.
  187. Status is not 404 from a missing static file.
  188. ### 3. Static assets
  189. ```bash
  190. curl -sS -D - -o /dev/null \
  191. "https://pnspsdev.gld.gov.hk/static/js/main.<hash>.chunk.js"
  192. ```
  193. Use a real hash from the deployed `index.html`. Expect **200**.
  194. ### 4. Other client routes
  195. ```bash
  196. curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/login"
  197. curl -sS -D - -o /dev/null "https://pnspsdev.gld.gov.hk/dashboard"
  198. ```
  199. Expect **200** HTML shell when logged-out routes are publicly reachable
  200. (may redirect in browser; curl shows initial response).
  201. ### 5. Browser — card payment end-to-end
  202. 1. Start card payment; complete or cancel on GCIS gateway.
  203. 2. DevTools → Network: document for `/paymentPage/callback?...` → **200**.
  204. 3. Network: `POST .../api/payment/api/updatepayment` and
  205. `POST .../api/payment/callback/status` → success responses per business rules.
  206. 4. UI: success, cancel, or fail page from `PaymentCallback.js`.
  207. ### 6. Apache reload
  208. ```bash
  209. sudo apachectl configtest
  210. sudo systemctl reload apache2
  211. ```
  212. Use `httpd` / `apachectl` as on your GLD hosts.
  213. ---
  214. ## Application behaviour (after shell loads)
  215. ### Auth gate
  216. `paymentPage/callback` is under `PublicUserRoutes`, which is registered
  217. only when the user is logged in as IND or ORG (`src/routes/index.js`).
  218. If the session expires on the gateway, the user may hit `/login` instead
  219. of the callback page. That is separate from the document 404; test with a
  220. fresh login if callback APIs never fire.
  221. ### FPS vs card
  222. | Flow | Return path | Route file |
  223. |---|---|---|
  224. | Card | `/paymentPage/callback` | `PublicUserRoutes.js` |
  225. | FPS (PASG) | `/paymentPage/fps/fpscallback` | `PublicUserRoutes.js` (+ test route in `LoginRoutes.js`) |
  226. This follow-up focuses on **card** `returnurl` to `/paymentPage/callback`.
  227. ### Code fragility (optional app fix, not Apache)
  228. `PaymentCallback.js` uses
  229. `params.get("RECEIPT_TOKEN").replace("%3D", "=")` without a null check.
  230. Missing `RECEIPT_TOKEN` throws before API calls. Harden separately if
  231. gateways ever omit the param.
  232. ---
  233. ## What not to do
  234. | Idea | Why |
  235. |---|---|
  236. | Add only a React route | Does not fix the first GET to Apache/Tomcat |
  237. | `Redirect` callback URL to `/` | Loses query params unless carefully preserved |
  238. | Rewrite `/api/` to `index.html` | Breaks payment POSTs, logout, login, etc. |
  239. | Rely on 404 + `index.html` body | Payment may work; monitoring and CSP reports stay noisy |
  240. | Change CSP for this | Not a CSP block; document status is HTTP routing |
  241. ---
  242. ## Decision summary
  243. | Option | Where SPA fallback lives | Remove `ProxyPass /`? |
  244. |---|---|---|
  245. | A | Apache `DocumentRoot` + `FallbackResource` | Yes (for main site) |
  246. | B | Apache `RewriteRule` → `/index.html` + existing proxy | No |
  247. | C | Tomcat / origin on 8080 | No Apache change |
  248. **Recommended:** Option A if operations can publish `build/` to Apache;
  249. Option B if the static tree must remain on `192.168.8.6:8080`.
  250. After implementation, `/paymentPage/callback` should show **200** on the
  251. document request while behaviour of `PaymentCallback.js` and payment APIs
  252. stays the same.
  253. ---
  254. ## Code map (quick reference)
  255. | Piece | Location |
  256. |---|---|
  257. | Callback route | `PNSPS-frontend-MaterialUI/src/routes/PublicUserRoutes.js` |
  258. | Callback UI + API calls | `PNSPS-frontend-MaterialUI/src/pages/Payment/PaymentCallback.js` |
  259. | Card return URL | `PNSPS-frontend-MaterialUI/src/pages/Payment/Card/index.js` |
  260. | API constants | `PNSPS-frontend-MaterialUI/src/utils/ApiPathConst.js` |
  261. | Route auth wiring | `PNSPS-frontend-MaterialUI/src/routes/index.js` |
  262. | Payment APIs | `PNSPS-backend/.../payment/web/PaymentController.java` |
  263. | API context path | `PNSPS-backend/src/main/resources/application.yml` (`/api`) |
  264. ---
  265. ## Change log (doc)
  266. | Date | Note |
  267. |---|---|
  268. | 2026-10-14 | Initial follow-up from payment callback 404 investigation and DEV vhost review |