概念定义
预检请求是浏览器在非简单跨源请求之前自动发出的 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++
}
}
}