monk24215 / url-redirect-php-request-forwarder
Framework-agnostic PHP HTTP request forwarder with retry, full passthrough, and pluggable logging.
Package info
github.com/monk24215/url-redirect-php-request-forwarder
pkg:composer/monk24215/url-redirect-php-request-forwarder
Requires
- php: >=8.0
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.5 || ^10.0
README
A small, framework-agnostic PHP library for forwarding HTTP requests with faithful passthrough, automatic retry, and pluggable logging.
What it does
- Forwards method, query string, body, headers, and cookies verbatim to any target URL
- Retries on transient failures (5xx, network errors) with exponential backoff
- Does not retry 4xx — those are client-side conditions that won't change
- Strips hop-by-hop headers (
Host,Content-Length,Connection, etc.) automatically - Returns a structured
ForwardResultobject (status, body, headers, attempts, duration, error) - Optional transparent proxy mode (echoes upstream response back to the caller)
- Pluggable logging — ships with file (JSONL), PDO (MySQL/Postgres/SQLite), null, or bring your own
What it does NOT do
- It cannot guarantee 100% delivery. Networks fail, targets go down, certificates expire. What this library guarantees is faithful passthrough, sensible retry behavior, and structured error reporting so your caller can make informed decisions.
- It is not a streaming proxy — request and response bodies are buffered in memory. For multi-gigabyte payloads use a different tool.
- It does not handle authentication for you — pass any auth headers in via the
headersoption.
Installation
composer require monk24215/url-redirect-php-request-forwarder
Requires PHP 8.0+, ext-curl, ext-json.
Quick start
Basic forward
use RequestForwarder\RequestForwarder; $rf = new RequestForwarder('https://api.example.com/webhook', [ 'method' => 'POST', 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode(['event' => 'order.created']), ]); $resp = $rf->forward(); if ($resp->ok) { echo "Forwarded successfully in {$resp->durationMs}ms"; } else { error_log("Forward failed after {$resp->attempts} attempts: {$resp->error}"); }
Transparent proxy (relay incoming request to upstream)
Drop this as index.php in any document root and every request to that domain will be proxied to the target, with the URL bar unchanged:
<?php require __DIR__ . '/vendor/autoload.php'; use RequestForwarder\RequestForwarder; RequestForwarder::fromIncomingRequest('https://upstream.example.com/endpoint') ->proxy();
This auto-detects the incoming request's method, query, body, headers, and cookies, forwards them, and echoes the upstream response back to the caller.
Pair with this .htaccess to route every path through index.php:
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^.*$ index.php [QSA,L]
With file logging
use RequestForwarder\RequestForwarder; use RequestForwarder\Logger\FileLogger; $logger = new FileLogger(__DIR__ . '/logs/forwards.jsonl'); $rf = new RequestForwarder('https://api.example.com', [], $logger); $rf->forward();
With database logging
Apply sql/schema.sql to your database, then:
use RequestForwarder\Logger\PdoLogger; $pdo = new PDO('mysql:host=localhost;dbname=mydb', $user, $pass); $logger = new PdoLogger($pdo); $rf = new RequestForwarder('https://api.example.com', [], $logger);
Custom logger
Implement LoggerInterface:
use RequestForwarder\Logger\LoggerInterface; use RequestForwarder\ForwardResult; final class MyLogger implements LoggerInterface { public function log(array $request, ForwardResult $result): void { // ship to Monolog / Sentry / Datadog / etc. } }
Configuration
All options passed via the second constructor argument:
| Option | Default | Description |
|---|---|---|
method |
'GET' |
HTTP method |
query |
[] |
Query params (merged into target URL) |
body |
'' |
Request body (string) |
headers |
[] |
Request headers (['Name' => 'Value']) |
cookies |
[] |
Cookies (merged into Cookie header) |
timeout |
30 |
Total timeout (seconds) |
connect_timeout |
10 |
Connection timeout (seconds) |
max_retries |
3 |
Max attempts on 5xx/network errors |
retry_delay_ms |
250 |
Base delay (exponential backoff) |
follow_redirects |
true |
Follow 3xx |
max_redirects |
5 |
Redirect cap |
verify_ssl |
true |
Verify peer + host (leave on in production) |
strip_headers |
hop-by-hop list | Headers to strip before forwarding |
source_label |
null |
Tag for log entries |
user_agent |
'url-redirect-php-request-forwarder/1.0' |
Sent if no User-Agent header provided |
auto_detect_incoming |
false |
Populate from $_SERVER/$_GET/php://input |
ForwardResult
$resp->ok // bool — true if 2xx/3xx and no transport error $resp->status // int — HTTP status code (0 if request never completed) $resp->headers // array — response headers (repeated headers become arrays) $resp->body // string — response body $resp->attempts // int — how many tries it took $resp->durationMs // int — total wall-clock time $resp->finalUrl // string — URL actually requested (with merged query) $resp->error // ?string — error message if !ok $resp->json() // mixed — convenience JSON decode of body $resp->toArray() // array — full result as array
Retry semantics
- 5xx and network errors are retried up to
max_retriestimes with exponential backoff (retry_delay_ms * 2^(attempt-1)) - 4xx is NOT retried — these indicate a client-side condition (bad request, auth, not found) that won't change between retries
- 2xx/3xx returns immediately
- A failure after all retries still returns a
ForwardResult(not an exception) withok=falseand the last error
Security notes
- Leave
verify_sslon in production. Disabling it exposes you to MITM attacks. - Be careful when using
fromIncomingRequest()on a public endpoint — you become a proxy. Restrict which targets are allowed, rate-limit, and authenticate callers. - Request and response bodies may contain sensitive data. Configure your logger's body cap accordingly, or implement a redacting logger.
Testing
composer install
composer test
Tests hit httpbin.org and require outbound HTTP. To run offline, you'll need to mock cURL or use a local HTTP server.
Sibling project
Need this for Node.js? See url-redirect-request-forwarder-node — same surface, idiomatic Node, zero dependencies.
Contributing
Pull requests welcome. Please:
- Open an issue first for non-trivial changes
- Add tests for new behavior
- Follow PSR-12
License
MIT — see LICENSE.