[HTTP] API Timeout 指南 (connect / read timeout)

Intro

關於 API Timeout 的網路資料略顯不足,Wikipedia 目前也只有廣義 Timeout (computing) 資訊。

本篇將分享個人對 Timeout 的所知,如有錯誤歡迎告知修正


Timeout 定義

Client Connect Timeout Read Timeout
TCP Client TCP handshake 中從發出 SYN 後,
至等待收到 SYN-ACK 的時間
送出 PSH-ACK packet,
至等待收到對應 PSH-ACK packet 的時間
HTTP Client 從 DNS lookup (若有) 或 TCP handshake 中從發出 SYN 後,
至等待收到 SYN-ACK 的時間
(是否包含 DNS lookup 視套件而定)
送出最後 HTTP request packet,
至等待收到最後 response packet 的時間
HTTPS Client 從 DNS lookup (若有) 或 TCP handshake 中從發出 SYN 後,
至等待收到最後 TLS handshake 的時間
(是否包含 DNS lookup / TLS 交握視套件而定)
送出最後 HTTP request packet,
至等待收到最後 response packet 的時間
  • API timeout = Max timeout = Connect timeout + Read timeout

Programing

cURL

cURL 的 connect timeout 與 read timeout 會共用錯誤碼 28,但錯誤訊息內容不同,可藉此區分。

// connect timeout = 5, max timeout (connect+read) = 25
$ curl -v --connect-timeout 5 -m 25 "https://yourname.com"

// Result within connection timeout
curl: (28) Connection timed out after 5004 milliseconds
// Result within read timeout
curl: (28) Operation timed out after 25005 milliseconds with 0 bytes received

CURL - Timeouts · Everything curl
--connect-timeout - curl - How To Use: The connection phase is considered complete when the DNS lookup and requested TCP, TLS or QUIC handshakes are done.

Python

import requests

connect_timeout = 5; read_timeout = 20
try:
    response = requests.get("https://your.name", allow_redirects=False, timeout=(connect_timeout, read_timeout))
except requests.exceptions.ConnectTimeout:
    print("Connection Timeout")
except requests.exceptions.ReadTimeout:
    print("Read Timeout")

Timeouts - Advanced Usage — Requests documentation

PHP

PHP 是基於 OS 底層 CURL 函式庫作為 HTTP Client,可依照 CURL 的特性設定 timeout 並區分不同的 exceptions:

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://example.com',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 25,
]);

curl_exec($ch);
$errno = curl_errno($ch); // Error code
$error = curl_error($ch); // Error message
curl_close($ch);

if ($errno === CURLE_OPERATION_TIMEOUTED) { // Code 28
    if (str_contains($error, 'Connection timed out')) {
        echo "Connection timeout";
    } elseif (str_contains($error, 'Operation timed out')) {
        echo "Read timeout";
    } else {
        echo "Timeout occurred: $error";
    }
} elseif ($errno !== 0) {
    echo "❗ Other cURL error: $error\n";
} else {
    echo "✅ Request succeeded\n";
}

PHP with Guzzle

截至目前為止,Guzzle 的 Error Handler 並未進一步區分 cURL 中的 Connect Timeout 與 Read Timeout。
因此,當 cURL 回傳錯誤碼 CURLE_OPERATION_TIMEOUTED(代碼 28)時,Guzzle 會一律拋出 ConnectException。

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;

$client = new Client();

try {
    $response = $client->request('GET', 'https://your.name/', [
        'connect_timeout' => 5, // Connect timeout in seconds
        'timeout' => 25,        // Total (Connect + read) timeout in seconds
    ]);
} catch (ConnectException $e) {
    $message = $e->getMessage();
    if (str_contains($message, 'Connection timed out')) {
        return 'connect_timeout';
    }
    if (str_contains($message, 'Operation timed out')) {
        return 'read_timeout';
    }
    return 'unknown_timeout';
} 

JAVA with Apache HttpClient

根據目前官方文件,Apache HttpClient 的 Connect Timeout 主要涵蓋 TCP 連線建立階段,通常不包含 TLS 握手;TLS 握手階段的逾時則包含在 Socket Timeout 內。直到 5.x 版本,HttpClient 才新增獨立的 Response Timeout 用於控制伺服器回應的等待時間 (TBC)。

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.http.conn.ConnectTimeoutException;
import java.net.SocketTimeoutException;
import java.io.IOException;

public class ApacheHttpClient {
    public static void main(String[] args) {
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(5000)    // Connect timeout
                .setSocketTimeout(20000)    // (包含 TLS 握手、HTTP 回應等)
                .build();
        try (final CloseableHttpClient httpclient =
                     HttpClientBuilder.create().setDefaultRequestConfig(config).build()) {
            final HttpGet httpget = new HttpGet("https://your.name");
            try (CloseableHttpResponse response = httpclient.execute(httpget)) {
                HttpEntity entity = response.getEntity();
                if (entity != null) {
                    String content = EntityUtils.toString(entity);
                    System.out.println(content);
                }
            }
        } catch (ConnectTimeoutException e) {
            // Handle connect timeout
            e.printStackTrace();
        } catch (SocketTimeoutException e) {
            // Handle socket timeout (包含 TLS 握手、HTTP 回應等)
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
  • v 4.x - getConnectTimeout: Determines the timeout in milliseconds until a connection is established. (可能不含 TLS handshake)
  • v 4.x - getSocketTimeout: Defines the socket timeout (SO_TIMEOUT) in milliseconds, which is the timeout for waiting for data or, put differently, a maximum period inactivity between two consecutive data packets). (可能包含 TLS handshake)
  • v 5.5.x - setConnectTimeout: Determines the timeout until a new connection is fully established. This may also include transport security negotiation exchanges such as SSL or TLS protocol negotiation).
  • v 5.5.x - setResponseTimeout: Determines the timeout until arrival of a response from the opposite endpoint.

定義比較表

語言 / 工具 Connect timeout 是否包含 TLS? 是否可單獨控制 TLS timeout?
curl ✅ 包含 ❌ 無法細分
Python requests ✅ 包含 ❌ 無法細分
Go net/http ✅ 包含 ✅ 可獨立設定 TLSHandshakeTimeout
Node.js axios ✅ 包含 ❌ 無法細分
Node.js https 模組 ✅ 包含 ❌ 無法細分
Java HttpClient 4.x ❌ 不含(TLS 在 read/socket timeout) ❌ 無法細分
Java HttpClient 5.x ❌ 不含(TLS 在 read/socket timeout) ✅ 可透過 Response Timeout 控制

References

Leave a Reply

Your email address will not be published. Required fields are marked *