概念定义
重定向链是一串 3xx 响应:每一跳再指向另一个重定向,直到 200。除非连接已预热且命中缓存,每跳都要付出 DNS、建连与请求往返。状态码的缓存语义与方法保持各有差异,链长同时劣化延迟与爬虫权重。
工作机制
301 与 308 表示永久并鼓励长期缓存;302、303、307 是临时性、默认缓存更保守。方法保持也不同:308 与 307 保留方法与请求体,301 与 302 历史上允许把 POST 改写为 GET。浏览器会缓存链条,爬虫会封顶跳转深度。
使用场景
每次迁移或域名合并后都应审计。修复通常是把中间跳直接指向最终 URL、把内链更新到规范目标,并确保跟踪参数不再强制额外跳转。
常见误区
瀑布图里每一跳是独立请求,链长容易被忽略——五跳链条在 100 毫秒 RTT 的移动网络能烧掉一秒耐心。部分中间设备会剥除或改写重定向,行为因网络而异。循环往复是退化形态:应按入口 URL 监控重定向计数。
代码示例
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
type Shipment struct {
ID string `json:"id"`
Status string `json:"status"`
}
func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // 必须释放计时器资源
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
}
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
var shipment Shipment
if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
return nil, fmt.Errorf("decode body: %w", err)
}
return &shipment, nil
}
func main() {
s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
fmt.Println(s, err)
}
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"time"
)
type fetchResult struct {
Status int
Body string
}
func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fetchResult{}, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
}
return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return fetchResult{}, fmt.Errorf("read body: %w", err)
}
return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(200 * time.Millisecond):
fmt.Fprint(w, "late")
case <-r.Context().Done():
}
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
ctx := context.Background()
result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)
result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("slow request classified as a deadline error")
}
}