Spring Security & Testing/CORS — Same-Origin Policy, preflight, và Spring config
16/20
Bài 16 / 20~14 phútAuthorization & Web SecurityMiễn phí lượt xem

CORS — Same-Origin Policy, preflight, và Spring config

CORS (Cross-Origin Resource Sharing) không phải tấn công — đây là cơ chế browser tự vệ chặn JavaScript đọc response cross-origin. Bài này bóc origin tuple theo RFC 6454, khi nào browser kích hoạt preflight OPTIONS, cách Spring config allowedOrigins/Methods/Headers/Credentials, và vì sao spec cấm allowedOrigins(*) kết hợp allowCredentials(true).

TL;DR: CORS (Cross-Origin Resource Sharing, Fetch Standard) là policy browser áp lên JavaScript: nếu script ở origin A gọi API ở origin B (khác scheme, host, hoặc port), browser chặn JS đọc response trừ khi server B opt-in qua Access-Control-Allow-Origin header. Với "non-simple request" (JSON body, Authorization header), browser gửi preflight OPTIONS trước để hỏi server có chấp nhận không. Spring Security config CORS qua CorsConfigurationSource + http.cors(...)CorsFilter đăng ký sớm trong chain, trước AuthorizationFilter, để preflight không bị chặn bởi auth. Spec cấm tổ hợp allowedOrigins("*") + allowCredentials(true) vì tổ hợp đó cho phép mọi website attacker gọi API kèm cookie/token rồi đọc response — browser reject ngay.

1. Same-Origin Policy và origin tuple

1.1 Origin là gì theo RFC 6454

RFC 6454 — The Web Origin Concept định nghĩa origin là tuple ba thành phần (scheme, host, port). Hai URL chỉ cùng origin khi cả ba khớp chính xác:

URLOrigin
https://olhub.org/courses(https, olhub.org, 443)
https://olhub.org:8080/courses(https, olhub.org, 8080)
https://api.olhub.org/courses(https, api.olhub.org, 443)
http://olhub.org/courses(http, olhub.org, 80)

SPA tại https://olhub.org gọi API tại https://api.olhub.org — host khác — là cross-origin. Browser mặc định chặn JavaScript đọc response.

1.2 Vì sao Same-Origin Policy tồn tại

SOP giải quyết một vấn đề cụ thể: ngăn malicious site ăn cắp dữ liệu user đang đăng nhập.

Kịch bản không có SOP: bạn login bank.com xong mở tab mới vào evil.com. JS tại evil.com gọi fetch("https://bank.com/transactions") — browser tự đính kèm cookie bank.com (vì cookie theo domain, không theo origin của script). Không có SOP, evil.com đọc được số tài khoản, lịch sử giao dịch.

SOP can thiệp ở chiều đọc response, không phải chiều gửi request:

  1. Request vẫn đến server (lý do lịch sử — HTML form cross-site đã tồn tại từ trước khi JS xuất hiện).
  2. JS không được đọc response body, trừ khi server opt-in qua CORS header.

Phân biệt này quan trọng: SOP chặn đọc, không chặn gửi. Chính vì thế CSRF vẫn nguy hiểm ngay cả khi có SOP. Request POST transfer tiền đã đến server — attacker không cần đọc response để đạt mục đích. CORS và CSRF giải quyết hai chiều khác nhau của cùng mối đe dọa cross-site.

1.3 Same-Origin vs Same-Site — đừng nhầm

Khái niệmĐịnh nghĩaVí dụ cùng
Same-Originscheme + host + port đều khớphttps://olhub.org vs https://olhub.org
Same-SiteeTLD+1 (registrable domain) khớphttps://app.olhub.org vs https://api.olhub.org

Same-site lỏng hơn một bậc so với same-origin: app.olhub.orgapi.olhub.org là same-site (cùng olhub.org) nhưng cross-origin (host khác). Khái niệm same-site được dùng trong SameSite cookie attribute — cơ sở cho phòng thủ CSRF hiện đại, bóc ở bài tiếp theo: CSRF & khi nào tắt.

flowchart LR
  SPA["SPA<br/>https://olhub.org"]
  API["API<br/>https://api.olhub.org"]
  SPA -- "fetch /projects" --> API
  API -- "JS doc read?" --> SOP{"Same-Origin Policy<br/>host khac nhau"}
  SOP -- "khong opt-in" --> Block["Browser block<br/>JS read response"]
  SOP -- "ACAO header match" --> Allow["JS doc read OK"]

2. Preflight OPTIONS — khi nào và tại sao

2.1 Simple request vs non-simple request

Browser chia request thành hai loại. Simple request (không cần preflight) phải thỏa đồng thời:

  • Method là GET, HEAD, hoặc POST.
  • Header chỉ thuộc safe list: Accept, Accept-Language, Content-Language, Content-Type (giới hạn application/x-www-form-urlencoded, multipart/form-data, text/plain).

Mọi REST API hiện đại đều gửi Content-Type: application/json hoặc kèm Authorization: Bearer ... — cả hai đều khiến request là non-simple. Mọi POST/PUT/DELETE với JSON body đều preflight.

Lý do spec thiết kế preflight: với simple request (form HTML cổ điển), browser cứ gửi vì đó là hành vi web đã tồn tại từ những năm 1990. Với request mới (custom header, JSON body), browser hỏi trước để server có cơ hội từ chối trước khi side-effect xảy ra.

2.2 Flow preflight

sequenceDiagram
  participant B as Browser
  participant S as API Server

  Note over B: SPA goi fetch POST /api/projects<br/>header: Authorization + Content-Type JSON

  B->>S: OPTIONS /api/projects
  Note over B,S: Origin: https://olhub.org<br/>Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: Authorization,Content-Type

  S->>B: 200 OK
  Note over B,S: Access-Control-Allow-Origin: https://olhub.org<br/>Access-Control-Allow-Methods: GET,POST,PUT,DELETE<br/>Access-Control-Allow-Headers: Authorization,Content-Type<br/>Access-Control-Max-Age: 3600<br/>Vary: Origin

  Note over B: Preflight pass - cache 1h
  B->>S: POST /api/projects (request that)
  S->>B: 201 Created

Access-Control-Max-Age (giây) cho browser cache kết quả preflight cho cùng URL + method + header combination. Chrome cap 7200 giây, Firefox cap 86400. Đặt 3600 là an toàn cross-browser.

Header Vary: Origin rất quan trọng nếu response đi qua CDN: báo cache "key bắt buộc bao gồm cả giá trị Origin header". Thiếu Vary: Origin, CDN có thể trả response chứa Access-Control-Allow-Origin: https://olhub.org cho request từ https://evil.com — CORS fail bí ẩn, khó debug.

Cache preflight per-resource

Max-Age cache per URL, không global. OPTIONS /api/projectsOPTIONS /api/projects/42 là hai cache entry độc lập. App nhiều endpoint sẽ thấy preflight bùng phát ở lần warm cache đầu — đây là bình thường, không phải lỗi.

3. Spring CORS config

3.1 Tích hợp Spring Security (pattern đúng)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())  // stateless JWT API
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://olhub.org", "https://app.olhub.org"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-Id"));
        config.setExposedHeaders(List.of("X-Total-Count", "Location", "X-Request-Id"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

Khi gọi http.cors(...), Spring Security đăng ký CorsFilter vào vị trí rất sớm trong chain — trước AuthorizationFilter. Preflight OPTIONS được trả ngay tại CorsFilter, không bao giờ chạm tới auth logic. Đây là điểm khác biệt quan trọng so với cách config tiếp theo.

Tham khảo thêm cấu trúc SecurityFilterChain DSL và thứ tự filter tại SecurityFilterChain DSL.

3.2 Global qua WebMvcConfigurer — hạn chế khi dùng với Spring Security

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://olhub.org")
            .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

Hạn chế chính: cách này không đăng ký CorsFilter vào Spring Security filter chain. Preflight OPTIONS sẽ bị AuthorizationFilter chặn trả 401 trước khi CORS config được áp. Kết quả là frontend thấy CORS error trong khi thực tế là auth error.

Cách an toàn: luôn dùng http.cors(cors -> cors.configurationSource(...)) khi app có Spring Security.

3.3 Per-controller @CrossOrigin

@RestController
@RequestMapping("/api/widget")
@CrossOrigin(
    origins = "https://embed.olhub.org",
    methods = {RequestMethod.GET},
    maxAge = 3600
)
public class WidgetController { /* ... */ }

Override config global cho riêng controller. Phù hợp cho public widget hoặc endpoint embed cross-domain với rule khác phần còn lại của API.

4. Các thuộc tính CORS — bóc từng cái

4.1 allowedOrigins vs allowedOriginPatterns

// Exact list — recommend cho production single-tenant
config.setAllowedOrigins(List.of("https://olhub.org", "https://app.olhub.org"));

// Pattern — cho subdomain dong (multi-tenant SaaS), Spring 5.3+
config.setAllowedOriginPatterns(List.of("https://*.olhub.org"));

Khi dùng allowedOriginPatterns, Spring không trả * về browser. Thay vào đó nó match pattern với Origin gửi tới, rồi echo lại đúng origin đó — browser accept vì nhận exact origin, không phải wildcard.

4.2 allowedMethodsallowedHeaders

config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));  // hoặc liệt ke cu the

allowedMethods("*") chấp nhận được — rủi ro thấp vì Spring MVC routing vẫn enforce method whitelist bên dưới. allowedHeaders("*") tiện cho dev; production strict có thể liệt kê Authorization, Content-Type, các trace header.

4.3 exposedHeaders — hay bị quên

Mặc định JavaScript chỉ đọc được một số response header an toàn (Cache-Control, Content-Type, Content-Length...). Mọi header custom muốn JS đọc được đều phải khai báo:

config.setExposedHeaders(List.of("X-Total-Count", "Location", "X-Request-Id"));

Trường hợp hay gặp: POST /api/resources trả về Location: /api/resources/42 nhưng frontend gọi response.headers.get("Location") nhận null — vì Location chưa được expose.

"Credentials" trong CORS nghĩa là credential browser tự đính kèm: cookie (khi fetch với credentials: "include") hoặc HTTP Basic. JWT trong Authorization: Bearer ... header là explicit attach — JS tự set header trên từng request, browser không bao giờ tự gửi. Với CORS, Authorization chỉ là một header cần được phép qua allowedHeaders, không liên quan tới credentials flag.

Config đúng cho SPA dùng JWT header thuần (không cookie):

config.setAllowedOrigins(List.of("https://olhub.org"));   // origin cu the
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(false);   // khong cookie -> khong can credentials

Đặt allowCredentials(true) "cho chắc" là anti-pattern: nó mở thêm bề mặt (mọi cookie của origin đi kèm request cross-origin được phép) và khoá bạn khỏi wildcard origin. Chỉ bật khi app thật sự dùng cookie — session phụ trợ hoặc HttpOnly cookie JWT.

4.5 maxAge

config.setMaxAge(3600L);   // Chrome cap 7200, Firefox cap 86400

Dev nên đặt maxAge(0) để mỗi lần thay CORS rule không phải clear browser cache; production đặt 3600.

5. Cơ chế bên dưới — vì sao spec cấm allowedOrigins("*") + allowCredentials(true)

Đây là rule quan trọng nhất, và có lý do cụ thể từ attack chain — không phải rule tùy tiện.

Attack chain nếu spec cho phép tổ hợp này:

1. user login olhub.org — browser luu cookie session (HttpOnly, Secure)
2. user mo tab moi, vao evil.com
3. evil.com chay JS:
   fetch("https://api.olhub.org/user/profile", {
     credentials: "include"   // browser tu dinh kem cookie session
   }).then(r => r.json())
     .then(data => exfiltrate(data))  // doc ten, email, token, private data
4. Server tra: Access-Control-Allow-Origin: *
               Access-Control-Allow-Credentials: true
5. Browser (neu chap nhan): JS cua evil.com doc duoc response body
6. Ket qua: toan bo du lieu user bi ro ra cho evil.com

Tổ hợp * + credentials: true có nghĩa là "bất kỳ website nào cũng có thể gọi API này kèm cookie/auth của user và đọc response". Đây là universal account takeover — không cần khai thác lỗ hổng nào khác.

Fetch Standard (https://fetch.spec.whatwg.org/#cors-protocol) cấm rõ:

If credentials flag is set and Access-Control-Allow-Origin is *, return a network error.

Browser kiểm tra ở phía client — ngay cả khi server trả * + credentials, browser throw TypeError: Failed to fetch và không cho JS đọc response.

Fix — ba cách:

// 1. Exact list (recommend production)
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowCredentials(true);  // OK voi exact origin

// 2. Pattern cho subdomain dong
config.setAllowedOriginPatterns(List.of("https://*.olhub.org"));
config.setAllowCredentials(true);  // Spring echo exact origin, khong tra wildcard

// 3. Wildcard hop le — khi khong can credentials
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(false);  // public read-only API, khong can auth
flowchart TB
  Combo{"allowedOrigins(*)<br/>+ allowCredentials(true)?"}
  Combo -- "Co" --> Reject["Browser reject<br/>TypeError: Failed to fetch<br/>Fetch Standard violation"]
  Combo -- "Khong" --> Check{"Credentials = true<br/>va exact origin?"}
  Check -- "Co" --> OK["OK - browser chap nhan"]
  Check -- "Khong - wildcard" --> PublicOK["OK - public API<br/>khong credentials"]

6. CORS troubleshooting

Triệu chứngNguyên nhânFix
No 'Access-Control-Allow-Origin' headerOrigin chưa whitelistThêm vào allowedOrigins
Wildcard * + credentials errorSpec violationĐổi sang exact origins hoặc allowedOriginPatterns
Method PUT is not allowedPUT thiếu trong allowedMethodsThêm vào list
Header X-Trace-Id not allowedHeader custom thiếuThêm vào allowedHeaders
OPTIONS trả 401Auth filter chặn preflightDùng http.cors(...)CorsFilter đăng ký trước auth
Location header null trong JSHeader custom chưa exposeThêm vào setExposedHeaders
CORS ổn ở dev, fail productionOrigins khác per-envConfig per-environment (YAML profile)
CORS fail bí ẩn qua CDNCDN không giữ Vary: OriginConfig CDN forward Vary header

Verify nhanh bằng curl (loại bỏ browser cache):

curl -i -X OPTIONS https://api.olhub.org/api/projects \
    -H "Origin: https://olhub.org" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: Authorization,Content-Type"

# Expect trong response:
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: https://olhub.org
# Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
# Access-Control-Allow-Headers: Authorization,Content-Type
# Access-Control-Allow-Credentials: true
# Access-Control-Max-Age: 3600
# Vary: Origin

Liên hệ các bài khác

  • SecurityFilterChain DSL: CorsFilter là một trong 15+ filter của Spring Security chain. Bài đó giải thích thứ tự filter, vì sao CorsFilter phải đứng trước AuthorizationFilter, và cách dùng http.cors(...) để đăng ký đúng vị trí — thiếu hiểu biết này dẫn trực tiếp tới lỗi preflight 401.
  • CSRF & khi nào tắt: CORS và CSRF thường bị nhầm vì cùng liên quan cross-site. Bài này bóc tại sao CORS (chặn đọc response) và CSRF (forged request) là hai vấn đề độc lập — và vì sao stateless JWT API disable CSRF trong khi vẫn cần CORS.

Tóm tắt

  • Origin = (scheme, host, port) theo RFC 6454. Ba thành phần phải khớp chính xác mới cùng origin.
  • SOP tồn tại để chặn JS cross-origin đọc dữ liệu user. SOP chặn đọc response, không chặn gửi request — CSRF vẫn nguy hiểm vì thế.
  • Preflight OPTIONS kích hoạt với non-simple request (JSON body, Authorization header). Browser hỏi server trước khi gửi request thật; kết quả cache theo Max-Age per-URL.
  • Spring config: http.cors(cors -> cors.configurationSource(...))CorsFilter đăng ký trước auth filter, preflight bypass authentication đúng cách.
  • Spec cấm allowedOrigins("*") + allowCredentials(true): tổ hợp cho phép mọi website attacker đọc response kèm cookie/token user — universal account takeover. Browser reject ngay với TypeError: Failed to fetch.
  • Fix wildcard + credentials: dùng exact origins list, allowedOriginPatterns cho subdomain động, hoặc wildcard chỉ khi credentials = false.
  • exposedHeaders cần khai báo rõ cho header custom muốn JS đọc được.
  • Vary: Origin trong response cần thiết khi response đi qua CDN/cache.

Tự kiểm tra

Tự kiểm tra
Q1
Frontend SPA tại https://olhub.org gọi API tại https://api.olhub.org. Browser hiện "blocked by CORS policy". Chỉ ra: (a) đây là cross-origin vì lý do gì theo RFC 6454, (b) tại sao browser chặn, (c) bước đầu tiên để fix là gì?

(a) RFC 6454 định nghĩa origin là tuple (scheme, host, port). https://olhub.org có host olhub.org; https://api.olhub.org có host api.olhub.org. Host khác nhau nên là cross-origin, dù cùng scheme (https) và cùng port (443).

(b) Same-Origin Policy của browser chặn JavaScript đọc response cross-origin theo mặc định — cơ chế bảo vệ chống malicious site đọc dữ liệu user từ API khác. Server chưa opt-in qua Access-Control-Allow-Origin header nên browser không cho JS đọc response body.

(c) Bước đầu tiên: thêm origin vào Spring CORS config. Gọi http.cors(cors -> cors.configurationSource(corsConfigurationSource())) và trong corsConfigurationSource() đặt config.setAllowedOrigins(List.of("https://olhub.org")). Verify bằng curl OPTIONS request — phải thấy Access-Control-Allow-Origin: https://olhub.org trong response header.

Q2
Giải thích khi nào browser gửi preflight OPTIONS và khi nào không. Một request POST /api/projects với Content-Type: application/jsonAuthorization: Bearer eyJ... có preflight không? Tại sao spec thiết kế preflight?

Browser gửi preflight khi request là non-simple: method ngoài GET/HEAD/POST, hoặc có header custom, hoặc Content-Type không thuộc ba giá trị form chuẩn. Simple request (form HTML cổ điển GET/POST plain text) không cần preflight vì đó là hành vi web đã có từ trước khi JS xuất hiện.

Request POST /api/projects với Content-Type: application/jsonAuthorization header là non-simple trên cả hai tiêu chí: application/json không phải Content-Type form chuẩn, và Authorization là header custom. Browser sẽ gửi preflight OPTIONS trước.

Lý do spec thiết kế preflight: với simple request (form HTML), browser cứ gửi vì hành vi đó đã tồn tại từ những năm 1990 — thay đổi sẽ phá vỡ web cũ. Với request mới (JSON body, custom header), browser hỏi server trước để server có cơ hội từ chối trước khi side-effect xảy ra. Nếu preflight bị từ chối, request thật không bao giờ gửi đi.

Q3
Tại sao spec Fetch Standard cấm tổ hợp allowedOrigins("*") + allowCredentials(true)? Mô tả attack chain cụ thể nếu tổ hợp này được phép, và liệt kê ba cách fix.

Attack chain: user login olhub.org, browser lưu cookie session. User mở evil.com. JS tại evil.com gọi fetch("https://api.olhub.org/user/profile", { credentials: "include" }) — browser tự đính kèm cookie olhub.org. Nếu server trả Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true và browser chấp nhận, JS của evil.com đọc được toàn bộ response body: tên, email, token, dữ liệu nhạy cảm. Đây là universal account takeover — không cần khai thác lỗ hổng nào khác, chỉ cần user truy cập trang attacker.

Fetch Standard cấm tổ hợp này: nếu credentials flag được set và server trả Access-Control-Allow-Origin: *, browser trả về network error — fetch promise reject với TypeError: Failed to fetch.

Ba cách fix:

(1) Exact origins list: config.setAllowedOrigins(List.of("https://olhub.org")) + config.setAllowCredentials(true). Đơn giản, audit-friendly, recommend cho production single-tenant.

(2) allowedOriginPatterns (Spring 5.3+): config.setAllowedOriginPatterns(List.of("https://*.olhub.org")) + setAllowCredentials(true). Spring match pattern rồi echo lại exact origin — không trả wildcard về browser, nên hợp lệ với spec. Dùng cho multi-tenant SaaS với subdomain động.

(3) Wildcard không credentials: config.setAllowedOrigins(List.of("*")) + setAllowCredentials(false). Hợp lệ cho public read-only API không cần auth — ví dụ public stats API, open data. Spec chỉ cấm tổ hợp wildcard + credentials, không cấm wildcard riêng.

Q4
Bạn config CORS qua WebMvcConfigurer.addCorsMappings() nhưng preflight OPTIONS vẫn trả 401. Giải thích cơ chế tại sao, và cách fix đúng với Spring Security.

Cơ chế lỗi: WebMvcConfigurer.addCorsMappings() config CORS ở tầng Spring MVC — tầng này xử lý request sau khi đã qua toàn bộ Spring Security filter chain. AuthorizationFilter là filter cuối cùng trong Security chain, kiểm tra authentication/authorization trước khi request đến tầng MVC. Preflight OPTIONS không có credential — bearer token, session cookie — nên AuthorizationFilter từ chối ngay với 401. Request không bao giờ đến tầng MVC, CORS config không bao giờ được áp.

Fix đúng: dùng http.cors(cors -> cors.configurationSource(corsConfigurationSource())) trong SecurityFilterChain. Khi gọi method này, Spring Security đăng ký CorsFilter vào Security chain ở vị trí rất sớm — trước AuthorizationFilter. Preflight OPTIONS được xử lý tại CorsFilter, trả 200 với CORS headers, không bao giờ chạm tới auth logic.

Để verify thứ tự filter, bật log: logging.level.org.springframework.security: DEBUG. Log sẽ cho thấy CorsFilter invoke trước AuthorizationFilter. Nếu thấy ngược lại — hoặc CorsFilter không xuất hiện — config sai.

Q5
setExposedHeaders khác setAllowedHeaders ở điểm gì? Cho ví dụ tình huống cần dùng setExposedHeaders và hậu quả nếu quên.

Khác biệt:

setAllowedHeaders khai báo header nào client (browser) được phép gửi trong request — liên quan đến preflight. Browser gửi Access-Control-Request-Headers trong OPTIONS; server phải echo lại trong Access-Control-Allow-Headers; nếu header không được phép, preflight fail.

setExposedHeaders khai báo response header nào JavaScript được phép đọc. Mặc định JS chỉ đọc được "CORS-safelisted" response headers: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma. Mọi header ngoài danh sách này — kể cả header server đã trả — JS nhận null khi gọi response.headers.get().

Ví dụ cụ thể: endpoint POST /api/projects trả Location: /api/projects/42X-Total-Count: 150. Frontend muốn redirect tới resource mới: response.headers.get("Location"). Nếu chưa khai báo setExposedHeaders(List.of("Location", "X-Total-Count")), cả hai call đều trả null — header server đã gửi nhưng browser giấu JS.

Hậu quả: bug âm thầm — không có error, chỉ là null. Dev thường mất nhiều thời gian debug vì nhìn DevTools thấy header trong response, nhưng JS log lại null. Fix: thêm header vào setExposedHeaders.

Bài tiếp theo: CSRF & khi nào tắt

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên