Wp http api client
Skill Lonsdale201/wp-agent-skills/wordpress/wp-http-api-client
A community-maintained collection of agent skills for WordPress plugin and theme development.
npx -y skills add Lonsdale201/wp-agent-skills --skill wp-http-api-clientAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Implement or audit outbound HTTP integrations in WordPress with wp_remote_request, wp_safe_remote_get/post/request, bounded timeouts, redirects and response sizes, host allowlists, JSON handling, authentication redaction, retries, idempotency, streaming downloads, and test hooks. Use when a plugin calls an external API, webhook destination, feed, license server, OAuth endpoint, remote file, private update service, remote report definition, or accepts a URL that WordPress fetches.
SKILL.md
10.4 KB, as published. Nobody here has run it
WordPress HTTP API Client
Use the WordPress HTTP API instead of cURL or URL-enabled filesystem calls.
Core selects a transport, verifies TLS by default, follows WordPress proxy and
debug hooks, and returns a consistent array|WP_Error contract.
Pick the request function
| Situation | Function |
|---|---|
| Fixed, plugin-owned URL | wp_remote_get/post/request() after an exact host configuration check |
| User/admin-influenced URL | wp_safe_remote_get/post/request() |
| Large file download | download_url() or stream => true with cleanup |
| Inbound webhook | A REST route with signature/replay verification, not an HTTP client call |
The wp_safe_remote_* variants set reject_unsafe_urls and validate the
initial destination plus redirects with wp_http_validate_url(). Prefer them
whenever any part of the URL is configurable.
A production JSON request
function myplugin_fetch_customer( int $customer_id ) {
$base = untrailingslashit( (string) get_option( 'myplugin_api_base' ) );
$parts = wp_parse_url( $base );
if ( 'https' !== ( $parts['scheme'] ?? '' )
|| 'api.example.com' !== strtolower( $parts['host'] ?? '' ) ) {
return new WP_Error( 'myplugin_invalid_api_host', 'Invalid API host.' );
}
$response = wp_safe_remote_get(
$base . '/v1/customers/' . rawurlencode( (string) $customer_id ),
array(
'timeout' => 8,
'redirection' => 2,
'limit_response_size' => 256 * KB_IN_BYTES,
'headers' => array(
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . myplugin_get_api_key(),
),
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error(
'myplugin_api_transport',
__( 'The remote service could not be reached.', 'myplugin' ),
array( 'cause' => $response->get_error_code() )
);
}
$status = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status ) {
return new WP_Error(
'myplugin_api_status',
__( 'The remote service returned an unexpected response.', 'myplugin' ),
array( 'status' => $status )
);
}
$content_type = (string) wp_remote_retrieve_header( $response, 'content-type' );
if ( ! str_starts_with( strtolower( $content_type ), 'application/json' ) ) {
return new WP_Error( 'myplugin_api_content_type', 'Expected JSON.' );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
return new WP_Error( 'myplugin_api_json', 'Invalid JSON response.' );
}
return $data;
}
Validate the decoded schema before using values. An HTTP 200 and valid JSON do not prove the expected fields/types are present.
Keep executable policy out of remote responses
Classify every response field as data or control-plane input. A fixed HTTPS host and valid authentication do not justify executing whatever that host returns.
Do not let a response directly provide:
- SQL passed to
$wpdb->query(),get_results(), or a report executor; - PHP/template content, callback/class names, local paths, or capabilities;
- an unrestricted plugin/theme update package URL;
- a redirect, webhook destination, or secondary download host outside an exact semantic allowlist.
Keep report queries and privileged decisions local and versioned. Prefer a small
remote vocabulary such as report_id, typed filter values, and feature states;
map the ID to local code after strict schema/enum/range validation. A SELECT
prefix check or SQL parser is not a durable sandbox, and read-only intent should
also be enforced with database privilege separation where practical.
For private updates, validate metadata and package hosts independently, recheck
every redirect, and verify a release signature/digest against a separately
trusted key or manifest. TLS protects the channel but does not contain a
compromised vendor. Apply wp-security-deep to rate the full SQL/update-to-code
execution chain and its trigger.
Sending JSON
$response = wp_safe_remote_post( $url, array(
'timeout' => 8,
'redirection' => 0,
'headers' => array(
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token,
'Idempotency-Key' => $operation_uuid,
),
'body' => wp_json_encode( $payload, JSON_UNESCAPED_SLASHES ),
) );
Do not send a PHP array as body while declaring JSON; WordPress will otherwise
form-encode it. Check wp_json_encode() failure for payloads that can contain
invalid UTF-8.
SSRF boundary
An exact HTTPS host allowlist is the strongest application-level rule. Match
the parsed host exactly, not with str_contains() or an unsafe suffix check
that accepts api.example.com.attacker.test. If subdomains are required,
accept the base host or a .-delimited suffix and still constrain scheme/port.
wp_safe_remote_* blocks many private/reserved destinations and revalidates
redirects, but core URL validation is not a complete defense against every
resolver, IPv6, DNS-rebinding, or hosting-network scenario. For arbitrary
user-selected URLs, combine it with short limits and infrastructure egress
controls/a fixed outbound proxy. Never disable the check through broad
http_request_host_is_external or http_allowed_safe_ports filters.
Time, size, and redirect budgets
- Set a short request-specific
timeout; the default is not a product SLA. - Cap
redirection, often at 0 for credential-bearing POSTs and 1-2 for GETs. - Set
limit_response_sizefor bounded text/JSON responses. - For large files, stream to a temporary file rather than buffering in PHP.
- Treat
blocking => falseas best-effort dispatch, not guaranteed delivery; no response body/status is available to prove remote acceptance. - Move slow/retriable integrations to Action Scheduler, WP-Cron, or a queue instead of blocking an admin/front-end request.
Errors, retries, and idempotency
Always handle both transport failure (WP_Error) and HTTP status. Do not leak
raw provider bodies to users; they can contain internals or reflected input.
Log a redacted request ID/status/error code, never Authorization/Cookie headers
or complete personal-data payloads.
Retry only bounded, transient failures such as selected network errors, 429,
and some 5xx responses. Honor Retry-After where practical and use backoff with
jitter. Never automatically retry a non-idempotent write unless the provider
supports an idempotency key or the operation has another deduplication gate.
Credentials and privacy
- Prefer Connectors, environment variables, or
wp-config.phpconstants for production credentials; DB settings are sometimes required for admin UX. - Never put secrets in URLs, because URLs leak into logs and caches.
- Require explicit feature/admin intent before spending quota or sending site content. A configured site-wide connector is not consent for every plugin.
- Document personal-data transfers and retention; run the privacy skill.
Downloads and cleanup
download_url() uses wp_safe_remote_get() and returns a temporary filename
or WP_Error. It does not install, validate, or delete the file for you.
require_once ABSPATH . 'wp-admin/includes/file.php';
$tmp = download_url( $url, 30 );
if ( is_wp_error( $tmp ) ) {
return $tmp;
}
try {
// Validate extension, real MIME, size, and product-specific content here.
return myplugin_import_downloaded_file( $tmp );
} finally {
if ( file_exists( $tmp ) ) {
wp_delete_file( $tmp );
}
}
Do not infer trust from a remote Content-Type or filename. Use the file upload
security skill before sideloading into Media Library.
Test and observe
- Use
pre_http_requestin tests to return deterministic fake responses. - Assert timeout, host, headers, body, error/status, malformed JSON, 429/5xx, oversized response, redirect, and cleanup behavior.
- Inject syntactically valid but malicious control fields (
sql,download_url, callback/path values) and assert they cannot reach an executable sink. - Use
http_api_debugonly for redacted diagnostics; never dump full requests. - Do not set
sslverify => false, including in local examples. Fix CA/proxy configuration instead.
Critical rules
- Use WP HTTP functions; never raw cURL or
file_get_contents( $url ). - Use
wp_safe_remote_*plus an allowlist for configurable destinations. - Bound timeout, redirects, and response size; handle
WP_Errorand status. - Keep TLS verification enabled and redact secrets/personal data.
- Retry only when the operation is demonstrably idempotent.
- Validate decoded response structure and downloaded file content.
- Keep executable SQL, update trust, callbacks, and authorization policy local.
Cross-references
- Use
wp-security-deepfor SSRF review. - Use
wp-security-secretsandwp-connectors-apifor credentials. - Use
wp-file-upload-securityfor downloaded/sideloaded files. - Use
wp-privacy-personal-datafor external personal-data transfers.
Core references
wp-includes/http.php: safe wrappers and URL validation.wp-includes/class-wp-http.php: request arguments and response contract.wp-admin/includes/file.php:download_url()and temporary-file cleanup.
References
- Official documentation: https://developer.wordpress.org/plugins/http-api/
- Official documentation: https://developer.wordpress.org/reference/functions/wp_safe_remote_request/
- Official documentation: https://developer.wordpress.org/reference/classes/wp_http/request/