| 623 | } |
| 624 | |
| 625 | async function toStableBody(init) { |
| 626 | const b = init && init.body; |
| 627 | if (!b) return ''; |
| 628 | try { |
| 629 | if (typeof b === 'string') { |
| 630 | // try JSON |
| 631 | try { |
| 632 | const j = JSON.parse(b); |
| 633 | // remove volatile fields like csrf |
| 634 | delete j.csrf; delete j.csrf_token; delete j._; |
| 635 | return 'JSON:' + JSON.stringify(j, Object.keys(j).sort()); |
| 636 | } catch (e) { |
| 637 | // maybe urlencoded |
| 638 | const p = new URLSearchParams(b); |
| 639 | ['csrf', 'csrf_token', '_'].forEach(k => p.delete(k)); |
| 640 | return 'FORM:' + [...p.entries()].map(([k, v]) => `${k}=${v}`).sort().join('&'); |
| 641 | } |
| 642 | } |
| 643 | } catch (e) { } |
| 644 | return 'B:' + String(b); |
| 645 | } |
| 646 | |
| 647 | window.fetch = async (input, init = {}) => { |
| 648 | // Base-path aware: if mounted under /fr, rewrite /api/* -> /fr/api/* |