WordPress’s HTTP API (wp_remote_get(), wp_remote_post(), etc.) returns either a WP_Error on connection failure or an associative array containing the response status code, headers, and body. The companion helper functions — wp_remote_retrieve_response_code(), wp_remote_retrieve_headers(), and wp_remote_retrieve_body() — parse this response array safely, returning sensible defaults when the key is absent. Calling these helpers instead of accessing the response array keys directly is the correct pattern: it avoids PHP notices on unexpected response shapes and makes code compatible with any HTTP transport (cURL, fsockopen, or WordPress’s test mock transport). A complete HTTP response handling workflow includes: checking for WP_Error first, validating the status code, checking Content-Type, parsing the body (JSON decode with error checking), and handling rate limiting headers.
Problem: A plugin calls an external JSON API that can return 200 (success), 401 (auth error), 429 (rate limited with a Retry-After header), or 5xx (server error). The plugin needs to handle each case, parse the JSON body safely, and cache successful responses — storing the rate limit reset time in a transient when rate limited.
Solution: Check for WP_Error, retrieve the status code and body via helper functions, switch on the status code, and use wp_remote_retrieve_header() for the Retry-After value.
<?php
function fetch_api_data( string $endpoint ): array|WP_Error {
$cache_key = 'api_data_' . md5( $endpoint );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$response = wp_remote_get( $endpoint, [
'timeout' => 10,
'headers' => [
'Authorization' => 'Bearer ' . get_option( 'my_api_key' ),
'Accept' => 'application/json',
],
] );
// ── Step 1: Check for connection error ────────────────────────────
if ( is_wp_error( $response ) ) {
// $response->get_error_message() = 'cURL error 28: Operation timed out'
return $response;
}
// ── Step 2: Read status code ──────────────────────────────────────
$status = (int) wp_remote_retrieve_response_code( $response );
// ── Step 3: Handle rate limiting ──────────────────────────────────
if ( 429 === $status ) {
$retry_after = (int) wp_remote_retrieve_header( $response, 'retry-after' );
$retry_after = max( 60, $retry_after ); // at least 60 seconds
set_transient( 'api_rate_limited_until', time() + $retry_after, $retry_after );
return new WP_Error(
'api_rate_limited',
sprintf( __( 'Rate limited. Try again in %d seconds.', 'textdomain' ), $retry_after )
);
}
// ── Step 4: Handle auth error ─────────────────────────────────────
if ( 401 === $status ) {
return new WP_Error( 'api_auth', __( 'API authentication failed. Check your API key.', 'textdomain' ) );
}
// ── Step 5: Handle server errors ──────────────────────────────────
if ( $status >= 500 ) {
return new WP_Error( 'api_server', sprintf( __( 'API server error (HTTP %d).', 'textdomain' ), $status ) );
}
// ── Step 6: Read and decode body ──────────────────────────────────
$body = wp_remote_retrieve_body( $response );
if ( '' === $body ) {
return new WP_Error( 'api_empty', __( 'API returned an empty response.', 'textdomain' ) );
}
$data = json_decode( $body, true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
return new WP_Error(
'api_json',
sprintf( __( 'JSON parse error: %s', 'textdomain' ), json_last_error_msg() )
);
}
// ── Step 7: Check Content-Type if needed ──────────────────────────
$content_type = wp_remote_retrieve_header( $response, 'content-type' );
// $content_type might be 'application/json; charset=utf-8'
// ── Step 8: Cache successful response ─────────────────────────────
if ( 200 === $status ) {
set_transient( $cache_key, $data, 15 * MINUTE_IN_SECONDS );
}
return $data;
}
// ── Usage ──────────────────────────────────────────────────────────────
$result = fetch_api_data( 'https://api.example.com/v1/items' );
if ( is_wp_error( $result ) ) {
echo esc_html( $result->get_error_message() );
} else {
foreach ( $result['items'] ?? [] as $item ) {
echo esc_html( $item['name'] ) . '<br>';
}
}
NOTE: wp_remote_retrieve_header() (singular) returns a single header value as a string; wp_remote_retrieve_headers() (plural) returns all headers as a Requests_Utility_CaseInsensitiveDictionary object that can be iterated or converted to an array. Header names are case-insensitive in HTTP — the WordPress helper normalises them to lowercase, so always pass lowercase header names: 'content-type', not 'Content-Type'. The wp_remote_get() timeout argument is in seconds and defaults to 5 — increase it for slow external APIs, but keep it reasonable (≤30 s) to avoid holding up PHP worker processes. For non-blocking background HTTP requests, use 'blocking' => false in the args array — the response body will be empty, but WordPress will send the request and immediately return.