無論是串接第三方 API、實現反向代理、模擬瀏覽器行為、或是進行安全測試,精確控制 HTTP 請求標頭與解析回應標頭都是 PHP 開發者的必備技能。本文系統梳理 cURL 標頭操作全流程。
| 選項 | 用途 | 典型值 |
|---|---|---|
CURLOPT_HTTPHEADER | 設定請求標頭陣列 | ['Content-Type: application/json', 'Authorization: Bearer xxx'] |
CURLOPT_HEADER | 是否將回應標頭包含在輸出中 | 1 (包含) / 0 (不包含,預設) |
CURLOPT_HEADERFUNCTION | 自定義回應標頭處理回呼 | function($ch, $header) { ... return strlen($header); } |
CURLINFO_HEADER_OUT | 啟用後可透過 curl_getinfo 獲取實際發送的請求標頭 | 1 |
function sendWithCustomHeaders(string $url, array $headers, string $body = ''): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers, // 關鍵:設定請求標頭
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true, // 關鍵:回應包含標頭
CURLOPT_SSL_VERIFYPEER => false, // 僅測試環境使用
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
throw new RuntimeException("cURL 錯誤 ($errno): $error");
}
// 分離標頭與主體
$headerSize = $info['header_size'];
$responseHeaders = substr($response, 0, $headerSize);
$responseBody = substr($response, $headerSize);
return [
'status' => $info['http_code'],
'headers' => parseRawHeaders($responseHeaders),
'body' => $responseBody,
'sent_headers' => $info['request_header'] ?? null, // 需啟用 CURLINFO_HEADER_OUT
];
}
function parseRawHeaders(string $raw): array {
$headers = [];
foreach (explode("\r\n", $raw) as $line) {
if (strpos($line, ':') !== false) {
[$key, $value] = explode(':', $line, 2);
$key = strtolower(trim($key));
$headers[$key] = trim($value);
}
}
return $headers;
}
// 使用範例
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken,
'X-Request-ID: ' . uniqid('req_', true),
'Accept: application/json',
];
$result = sendWithCustomHeaders('https://api.example.com/v1/users', $headers, json_encode(['name' => 'John']));
print_r($result['headers']);
function fetchHeadersOnly(string $url): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true, // HEAD 請求,不下載主體
CURLOPT_HEADER => true, // 獲取標頭
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true, // 跟隨重定向
CURLOPT_TIMEOUT => 10,
]);
$headerData = curl_exec($ch);
curl_close($ch);
return parseRawHeaders($headerData);
}
// 解析 Set-Cookie 多值標頭
function parseSetCookie(string $rawHeaders): array {
$cookies = [];
if (preg_match_all('/^Set-Cookie:\s*(.+)$/mi', $rawHeaders, $matches)) {
foreach ($matches[1] as $cookieLine) {
parse_str($cookieLine, $parts); // 簡易解析,實際建議用專門庫
$cookies[] = $parts;
}
}
return $cookies;
}
class BrowserSimulator {
private $ch;
private $cookieFile;
public function __construct() {
$this->cookieFile = sys_get_temp_dir() . '/cookies_' . uniqid() . '.txt';
$this->ch = curl_init();
curl_setopt_array($this->ch, [
CURLOPT_COOKIEJAR => $this->cookieFile,
CURLOPT_COOKIEFILE => $this->cookieFile,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 30,
]);
}
public function get(string $url, array $extraHeaders = []): array {
curl_setopt($this->ch, CURLOPT_HTTPGET, true);
curl_setopt($this->ch, CURLOPT_URL, $url);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, array_merge([
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: zh-TW,zh;q=0.9,en;q=0.8',
], $extraHeaders));
return $this->exec();
}
public function post(string $url, array $data, array $extraHeaders = []): array {
curl_setopt($this->ch, CURLOPT_POST, true);
curl_setopt($this->ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($this->ch, CURLOPT_URL, $url);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, array_merge([
'Content-Type: application/x-www-form-urlencoded',
'Origin: ' . parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST),
'Referer: ' . $url,
], $extraHeaders));
return $this->exec();
}
private function exec(): array {
$response = curl_exec($this->ch);
$info = curl_getinfo($this->ch);
if (curl_errno($this->ch)) {
throw new RuntimeException(curl_error($this->ch));
}
$headerSize = $info['header_size'];
return [
'status' => $info['http_code'],
'headers' => parseRawHeaders(substr($response, 0, $headerSize)),
'body' => substr($response, $headerSize),
'effective_url' => $info['url'],
];
}
public function __destruct() {
curl_close($this->ch);
@unlink($this->cookieFile);
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 自定義 Host 標頭無效 | cURL 自動從 URL 解析 Host | 使用 `CURLOPT_RESOLVE` 指定 IP 映射,或配合 `CURLOPT_HTTPHEADER` 設定 Host |
| Content-Length 重複 | 同時設定 POSTFIELDS 與 Content-Length | 勿手動設定 Content-Length,cURL 自動計算 |
| 回應標頭中文亂碼 | 伺服器未指定 charset 或編碼不一致 | 使用 `mb_detect_encoding` + `iconv` 轉換 |
| 大檔案下載記憶體爆炸 | CURLOPT_RETURNTRANSFER 緩存整個主體 | 使用 `CURLOPT_WRITEFUNCTION` 流式寫入檔案 |
function multiRequest(array $urls, int $maxConcurrent = 10): array {
$mh = curl_multi_init();
$handles = [];
$results = [];
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 10,
]);
curl_multi_add_handle($mh, $ch);
$handles[$i] = $ch;
}
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh, 0.5);
} while ($running > 0);
foreach ($handles as $i => $ch) {
$response = curl_multi_getcontent($ch);
$info = curl_getinfo($ch);
$headerSize = $info['header_size'];
$results[$i] = [
'url' => $urls[$i],
'status' => $info['http_code'],
'headers' => parseRawHeaders(substr($response, 0, $headerSize)),
'body' => substr($response, $headerSize),
];
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
掌握 cURL 標頭操作,能讓你從容應對 API 串接、爬蟲開發、微服務通訊、安全測試等場景。建議封裝為可複用的 HTTP Client 類別,整合 PSR-18 標準,提升專案可維護性。