使用 Windows WinInet API (InternetOpen → InternetConnect → HttpOpenRequest → HttpAddRequestHeaders → HttpSendRequest) 發送 POST 請求時,請求頭構造稍有不慎即導致 400 Bad Request。本文通過完整可編譯代碼,詳解請求頭格式規範、常見錯誤與 Wireshark 抓包對比分析法。
// 1. 檢查連通性
LPCTSTR lpURL = _T("https://192.168.8.72:8080");
if (!::InternetCheckConnection(lpURL, FLAG_ICC_FORCE_CONNECTION, 0)) return;
// 2. 打開會話
HINTERNET hOpen = ::InternetOpen(_T("client.exe"),
INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY, NULL, NULL, 0);
if (!hOpen) return;
// 3. 連接服務器 (注意端口 8080)
LPCTSTR lpDomain = _T("192.168.8.72");
HINTERNET hConnect = ::InternetConnect(hOpen, lpDomain, 8080,
NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
if (!hConnect) { InternetCloseHandle(hOpen); return; }
// 4. 創建請求句柄
LPCTSTR szAccept[] = { _T("*/*"), NULL };
DWORD dwFlags = INTERNET_FLAG_NO_CACHE_WRITE;
HINTERNET hRequest = ::HttpOpenRequest(hConnect, _T("POST"),
_T("/oss/client/analysis.g"), _T("HTTP/1.1"),
lpURL, szAccept, dwFlags, 0);
if (!hRequest) goto CLEANUP_CONNECT;
// 5. 準備 POST 體 (UTF-8)
TCHAR szPostData[] = _T("this is test data from client ");
char utf8PostData[1024] = {0};
strcpy(utf8PostData, dataConvert.TCharToUTF8(szPostData)); // 需自行實現轉換
// 6. 構造請求頭 —— 核心避坑區
TCHAR hdrLang[] = _T("Accept-Language: zh-CN,zh;q=0.8");
TCHAR hdrEnc[] = _T("Accept-Encoding: gzip,deflate,sdch");
TCHAR hdrCharset[] = _T("Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3");
TCHAR hdrType[] = _T("Content-Type: text/xml");
TCHAR hdrHost[] = _T("Host: 192.168.8.72:8080");
TCHAR hdrOrigin[] = _T("Origin: https://192.168.8.72:8080");
TCHAR hdrReferer[] = _T("Referer: https://192.168.8.72:8080/oss/client/create.g");
TCHAR hdrLen[64];
_stprintf(hdrLen, _T("Content-Length: %d\r\n\r\n"), strlen(utf8PostData)); // 雙 CRLF 結尾!
// 7. 逐個添加頭部 (ADD | REPLACE)
auto addHdr = [&](LPCTSTR h) {
HttpAddRequestHeaders(hRequest, h, -1,
HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE);
};
addHdr(hdrLang); addHdr(hdrEnc); addHdr(hdrCharset);
addHdr(hdrType); addHdr(hdrHost); addHdr(hdrOrigin);
addHdr(hdrReferer); addHdr(hdrLen);
// 8. 發送請求
BOOL bRet = ::HttpSendRequest(hRequest, NULL, 0,
utf8PostData, strlen(utf8PostData));
if (!bRet) { DWORD err = GetLastError(); goto CLEANUP_REQUEST; }
// 9. 讀取狀態碼與響應
TCHAR szBuf[256]; DWORD dwSize = sizeof(szBuf);
HttpQueryInfo(hRequest, HTTP_QUERY_STATUS_CODE, szBuf, &dwSize, NULL);
int nStatus = _tstoi(szBuf);
if (nStatus >= 200 && nStatus <= 206) {
// 讀取響應體...
InternetReadFile(hRequest, szBuf, sizeof(szBuf), &dwSize);
}
CLEANUP_REQUEST:
InternetCloseHandle(hRequest);
CLEANUP_CONNECT:
InternetCloseHandle(hConnect);
InternetCloseHandle(hOpen);
| 規則 | 錯誤示例 | 正確示例 | 原因 |
|---|---|---|---|
每個頭部單獨調用 HttpAddRequestHeaders | 拼接成單一大字串一次傳入 | 逐行 addHdr(hdrXxx) | WinInet 內部解析器要求獨立行 |
頭部值不帶 \r\n,最後一個頭部必須以雙 \r\n\r\n 結尾 | _T("Host: x\r\n") | _T("Host: x") … 最後 _T("Content-Length: 123\r\n\r\n") | 雙 CRLF 標誌頭部結束、體開始 |
Content-Length 必須為 Body 實際位元組長度 (UTF-8 後) | 用 _tcslen(szPostData) (字符數) | strlen(utf8PostData) | HTTP 傳輸按字節計算 |
http.request → 導出為 browser.txt。program.txt。Host/Content-Length/Expect: 100-continue| 狀態碼 | 典型原因 | 修正動作 |
|---|---|---|
| 400 Bad Request | 頭部格式錯、缺雙 CRLF、含不可見字符 | 對照 Wireshark 逐行核對 |
| 411 Length Required | POST 缺 Content-Length | 確保最後頭部含 Content-Length: N\r\n\r\n |
| 417 Expectation Failed | WinInet 默認加 Expect: 100-continue,服務端不支援 | 顯式添加 Expect: (空值) 覆蓋默認 |
WideCharToMultiByte(CP_UTF8)。HttpClient 類,暴露 Post(url, headers, body)。InternetSetOption(hRequest, INTERNET_OPTION_SEND_TIMEOUT, ...) / RECEIVE_TIMEOUT。INTERNET_FLAG_SECURE + INTERNET_FLAG_IGNORE_CERT_CN_INVALID (測試環境)。GetLastError() 對照 wininet.h / winerror.h 記錄日誌。來源:零一小築博客