Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Http Transport Laravel Package

innmind/http-transport

Innmind HTTP Transport is a PHP library for sending HTTP requests and handling responses through a clean, framework-agnostic API. Designed for the InnMind ecosystem, it keeps transport integration simple, testable, and lightweight for applications and services.

View on GitHub
Deep Wiki
Context7

Getting Started

innmind/http-transport is not a Laravel package and has no Laravel integration — no service provider, no facade, no config file, no Http::fake() equivalent. It is a low-level HTTP transport abstraction from the Innmind ecosystem: one interface, a handful of backends, and a set of small decorators you compose yourself. Treat it as an alternative to Guzzle's handler stack rather than an alternative to Laravel's Http client.

composer require innmind/http-transport

Requirements are strict (recent PHP 8.x, plus innmind/http, innmind/url, innmind/immutable). Check composer.json before promising it for a legacy app — it will not install on PHP 8.0/8.1 era projects.

Where to look first (in this order):

  1. src/Transport.php — the whole contract is essentially one method: __invoke(Request $request): Response (older majors: transport()). Everything else is decoration.
  2. src/Curl.php, src/Stream.php — the effectful backends. Pick Curl in normal apps; Stream only when the curl extension is unavailable.
  3. src/Exception/ — connection failures and (via a decorator) HTTP error types.
  4. The innmind/http package — Request, Response, Headers, Method value objects. This is where you'll spend most of your reading time, because constructing a request by hand is verbose.

First use case — a queued job or artisan command hitting a third-party API where you want immutable messages, explicit retry composition, and a trivially fakeable seam:

use Innmind\HttpTransport\Curl;
use Innmind\Http\Message\Request\Request;
use Innmind\Http\Message\Method\Method;
use Innmind\Url\Url;

$transport = Curl::of(/* response factory, where the major requires one */);

$response = $transport(new Request(
    new Method('GET'),
    Url::of('https://api.example.com/users/42'),
));

$status = $response->statusCode()->toInt();
$body   = $response->body()->toString();

Verify the exact constructor argument order against src/ the first time — the Innmind value objects are positional and unforgiving.

Implementation Patterns

1. Bind it behind your own interface, not raw. Don't inject Transport into twenty classes. Wrap it in a small app-level client so the Innmind types stay at the boundary:

final class BillingApi
{
    public function __construct(private Transport $transport) {}

    /** @return array<string, mixed> */
    public function invoice(string $id): array
    {
        $response = ($this->transport)($this->requestFor($id));

        return json_decode($response->body()->toString(), true, flags: JSON_THROW_ON_ERROR);
    }
}

Register it in a service provider with scoped() rather than singleton() (see Gotchas):

$this->app->scoped(BillingApi::class, fn () => new BillingApi(
    new ThrowOnError(new Retry(Curl::of(/* ... */), 3)),
));

2. Decorator order is the whole design. Retry only retries what it can see. If you want retries on 5xx, the error-mapping decorator must sit inside the retry:

// retries connection failures AND 5xx responses
new ThrowOnError(new Retry(new Curl(/* ... */), 3))

// retries connection failures only; 5xx throws after the first attempt
new Retry(new ThrowOnError(new Curl(/* ... */)), 3)

This is the single most common source of "why didn't it retry?" confusion, and it's invisible if you only read the config.

3. Testing with a hand-rolled fake. Because the contract is a single method, you don't need a mocking library or a Http::fake() analogue. Write this once per project:

final class FakeTransport implements Transport
{
    /** @var list<Request> */
    public array $requests = [];

    public function __construct(private Response $response) {}

    public function __invoke(Request $request): Response
    {
        $this->requests[] = $request;

        return $this->response;
    }
}

For per-call responses (or to simulate a 500 then a 200), swap the single Response for a queue. Testing retry logic against a fake transport is much cleaner than asserting on Guzzle's history middleware.

4. Cross-cutting concerns as decorators. The pattern the package is built for: metrics, logging, tracing, request signing, and caching are all Transport implementations wrapping the real one.

final class Timed implements Transport
{
    public function __construct(
        private Transport $inner,
        private LoggerInterface $logger,
    ) {}

    public function __invoke(Request $request): Response
    {
        $start = hrtime(true);

        try {
            return ($this->inner)($request);
        } finally {
            $this->logger->info('outbound http', [
                'method' => $request->method()->toString(),
                'url'    => $request->url()->toString(),
                'ms'     => (hrtime(true) - $start) / 1e6,
            ]);
        }
    }
}

5. Bridging to/from Laravel. Recent innmind/http messages implement the PSR-7 interfaces, so the escape hatch is mechanical: convert to PSR-7 and hand it to Illuminate\Http\Client\Response::fromPsrResponse() (or the reverse for feeding Laravel test fixtures into your fake). Keep that conversion in one adapter class — do not sprinkle new Request(...) construction across jobs.

6. Queue interplay. Let the queue own job-level retries ($tries, backoff()) and let the transport own connection-level retries, but cap the total. Three transport retries inside a job retried five times is 15 outbound calls to an API that's already down.

Gotchas and Tips

  • Pin exact versions. Innmind ships frequent majors in lockstep with innmind/http; changes that look minor (constructor arity, __invoke vs transport(), exception class names) land as breaking releases. Use "innmind/http-transport": "X.Y.Z" and read the changelog before any bump. The metadata listed here (a single star, zero dependents, a release dated in the future) means there is no community safety net: the source is small, so read src/ rather than searching for answers.

  • Don't register the transport as a singleton under Octane. Stateful decorators (circuit breakers, retry counters, rate limiters) and any curl handle held by the backend will leak across requests. scoped() per request, or a factory binding, keeps things honest.

  • The error model is not Laravel's. Connection failures throw; HTTP 4xx/5xx come back as ordinary Response values unless you add ThrowOnError (or equivalent). Catch specific Innmind\HttpTransport\Exception\* types, never bare \Exception, and always read the body off an error response before it's closed.

  • Bodies are streams. body()->toString() is a one-shot read in practice; if you call it twice, or after the response goes out of scope, you get nothing useful. Decode once and pass arrays around.

  • Headers are a strict collection of Header/Value objects, not an associative array. new Header('Accept', new Value('application/json')), and malformed names/values throw at construction. This is fine but adds boilerplate versus Http::withHeaders([...]).

  • No concurrency pool. Where Laravel gives you Http::pool(), this package gives you sequential calls. For fan-out over 20 endpoints, use Guzzle/Http::pool() or dispatch parallel jobs — don't try to compensate with retries.

  • Redirects and cookies are explicit. Verify whether your chosen backend follows redirects by default and whether a redirect decorator exists in your major; don't assume browser-like behaviour.

  • Debugging tip: the fastest diagnostic is a Timed-style decorator placed outside everything else, plus one that dumps $request->method(), $request->url(), and the response status on both success and exception paths. There is no built-in debug mode or log driver to enable.

  • Extension points are the point. Implement Transport for caching, metrics, or fakes; keep decorators stateless; keep exactly one effectful transport at the bottom of the stack. If you follow that rule the package behaves predictably — and if you don't, debugging a composed stack is considerably harder than debugging Guzzle middleware, because there's no profiler or history to inspect.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin