← 中文资源

简体中文版本

CORS 预检请求

CORS 预检是浏览器在某些跨源调用前自动发送的 OPTIONS 请求,先确认服务器允许的方法、头部与来源,再决定是否放行真实请求。

阅读英文原文

概念定义

预检请求是浏览器在非简单跨源请求之前自动发出的 OPTIONS 调用,携带 Origin、Access-Control-Request-Method 与 Access-Control-Request-Headers。服务器必须返回匹配的允许头,否则真实请求根本不会被发送。预检结果可经 Access-Control-Max-Age 缓存。

工作机制

只有经典方法加安全列表头部、表单类内容类型的请求才算简单请求。自定义头部(如 API Key)、JSON 请求体或 DELETE 方法都会触发预检。成功的预检响应按 Max-Age 秒数缓存,成本被摊销而不是每次调用都付。

使用场景

高延迟移动网络对预检最敏感:每次冷预检都在真实工作前增加整整一个往返。生产环境应设置较大的 Max-Age 并保持允许头部集合稳定。公开只读 API 可以用更简单的请求形态完全绕开预检。

常见误区

预检由浏览器生成和执行,不是服务端或应用代码发出的。反射任意 Origin 与请求头看似宽松,与凭证一起使用却极其危险。预检失败的报错看起来像 CORS 拒绝,其实真实请求从未发出。排查时应直接阅读网络面板中的 OPTIONS 交换记录。

代码示例

export function parseContentDisposition(header) {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const segments = splitParameters(header);
  const type = segments.shift().trim().toLowerCase();
  if (type !== "inline" && type !== "attachment") {
    throw new Error(`unsupported disposition type "${type}"`);
  }
  const parameters = new Map();
  for (const segment of segments) {
    const equals = segment.indexOf("=");
    if (equals === -1) continue;
    const name = segment.slice(0, equals).trim().toLowerCase();
    if (name === "") continue;
    parameters.set(name, unquote(segment.slice(equals + 1).trim()));
  }
  return { type, filename: resolveFilename(parameters), parameters };
}

function splitParameters(header) {
  const segments = [];
  let current = "";
  let quoted = false;
  for (const character of header) {
    if (character === '"') quoted = !quoted;
    if (character === ";" && !quoted) {
      segments.push(current);
      current = "";
      continue;
    }
    current += character;
  }
  segments.push(current);
  return segments;
}

function unquote(value) {
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
    return value.slice(1, -1).replace(/\\(.)/g, "$1");
  }
  return value;
}

function resolveFilename(parameters) {
  const extended = parameters.get("filename*");
  if (extended !== undefined) {
    const match = /^([\w-]+)'([\w-]*)'(.+)$/.exec(extended);
    if (match !== null) {
      try {
        return sanitize(decodeURIComponent(match[3]));
      } catch {
        return sanitize(extended);
      }
    }
  }
  const plain = parameters.get("filename");
  return plain === undefined ? null : sanitize(plain);
}

function sanitize(filename) {
  return filename.split(/[\\/]/).pop() || null;
}

console.log(parseContentDisposition('attachment; filename="report.pdf"'));
console.log(parseContentDisposition("attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf"));
console.log(parseContentDisposition('inline; filename="../../etc/passwd"'));
package main

import (
	"bufio"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"time"
)

type Broker struct {
	mu      sync.Mutex
	clients map[chan string]struct{}
}

func NewBroker() *Broker {
	return &Broker{clients: make(map[chan string]struct{})}
}

func (b *Broker) Subscribe() chan string {
	events := make(chan string, 8)
	b.mu.Lock()
	b.clients[events] = struct{}{}
	b.mu.Unlock()
	return events
}

func (b *Broker) Unsubscribe(events chan string) {
	b.mu.Lock()
	delete(b.clients, events)
	b.mu.Unlock()
}

func (b *Broker) Broadcast(event string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	for events := range b.clients {
		select {
		case events <- event:
		default:
		}
	}
}

func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.WriteHeader(http.StatusOK)
	flusher.Flush()

	events := b.Subscribe()
	defer b.Unsubscribe(events)
	for {
		select {
		case <-r.Context().Done():
			return
		case event := <-events:
			fmt.Fprintf(w, "data: %s\n\n", event)
			flusher.Flush()
		}
	}
}

func main() {
	broker := NewBroker()
	server := httptest.NewServer(broker)
	defer server.Close()

	go func() {
		time.Sleep(20 * time.Millisecond)
		broker.Broadcast("deploy started")
		time.Sleep(10 * time.Millisecond)
		broker.Broadcast("deploy finished")
	}()

	response, err := http.Get(server.URL)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()
	fmt.Println("content-type:", response.Header.Get("Content-Type"))

	scanner := bufio.NewScanner(response.Body)
	received := 0
	for scanner.Scan() && received < 2 {
		line := scanner.Text()
		if strings.HasPrefix(line, "data: ") {
			fmt.Println("event:", strings.TrimPrefix(line, "data: "))
			received++
		}
	}
}