skip to content

In Laravel, how do Http::macro() and baseUrl() build a reusable client for one third-party API, and why does Http::withToken($t); Http::get($url) lose the token?

level: middleimportance: should knowfreq 30%

answer

  1. Factory singleton, fresh PendingRequest
  2. macro registered in AppServiceProvider boot
  3. Http::carrier() returns a PendingRequest
  4. baseUrl joins with one slash
  5. absolute URLs ignore baseUrl

basics

~10 s

Register Http::macro('carrier', fn () => Http::baseUrl(...)->withToken(...)->timeout(5)) in a service provider's boot(), then call Http::carrier()->get('rates'). Each static Http:: call builds a new PendingRequest, so configuration lasts only within one chain.

solid answer

~30 s

The `Http` facade resolves `Illuminate\Http\Client\Factory`, a container singleton; every static call that is not a macro is forwarded to a brand-new `PendingRequest`. So `Http::withToken($t);` configures a request that is thrown away, and the next `Http::get()` starts clean. To reuse configuration, register a macro in `AppServiceProvider::boot()`: `Http::macro('carrier', fn () => Http::baseUrl(config('services.carrier.url'))->withToken(config('services.carrier.token'))->timeout(5))`. `Http::carrier()->get('rates')` then starts from that setup, and each call still gets a fresh object. `baseUrl()` is joined by trimming slashes and inserting one, so the base path prefix is kept; a URL starting with `http://` or `https://` bypasses it. `Http::globalOptions()` sets defaults for every request instead.

code

php · 19 lines
php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Http::macro('carrier', fn () => Http::baseUrl(config('services.carrier.url'))
            ->withToken(config('services.carrier.token'))
            ->acceptJson()
            ->timeout(5));
    }
}

// elsewhere: Http::carrier()->get('tracking/1Z999')->throw()->json('status');

go deeper

for a junior

Remember that each Http:: call starts a fresh request, so build the whole request in one chain, and that a macro gives you a named starting point.

for a middle

Explain how the Factory singleton forwards calls to new PendingRequest objects, how macros are checked first, and how baseUrl() joins relative paths.

for a senior

Show you structure integrations: one macro or client class per upstream, credentials from config, global options as a safety net, and no shared mutable PendingRequest.

for a principal

Weigh macros against dedicated client classes: macros are quick and discoverable on the facade, while classes give typed methods, a clear seam for testing and one owner per integration.

## Every Http:: call starts a new request `Illuminate\Support\Facades\Http` is a facade over `Illuminate\Http\Client\Factory`, which the framework registers as a **singleton** in the service container. The factory keeps app-wide settings (global options, global middleware, macros) but none of an individual request's configuration. Its `__call()` method does one of two things with any method you call through the facade: 1. if a **macro** with that name is registered, run the macro; 2. otherwise, create a **brand-new `PendingRequest`** and forward the call to it. That is why this code sends no token: ```php Http::withToken($token); // configures a request, then discards it Http::get('https://carrier.example/api/rates'); // a fresh request with no token ``` Configuration lives on the `PendingRequest` object, so it survives only along one fluent chain, or in a variable that holds that object. ## Building a per-API client with a macro The factory uses Laravel's `Macroable` trait, so you can give it a named method that returns a preconfigured `PendingRequest`. Register it once, usually in `AppServiceProvider::boot()`: ```php Http::macro('carrier', fn () => Http::baseUrl(config('services.carrier.url')) ->withToken(config('services.carrier.token')) ->acceptJson() ->timeout(5)); ``` Anywhere in the app — controllers, queued jobs, Artisan commands — `Http::carrier()->get('rates', ['zip' => $zip])` now starts from that configuration, and each call gets its own fresh object because the closure runs every time. Credentials come from `config()`, conventionally an entry you add to `config/services.php`. `PendingRequest` is macroable too, so `PendingRequest::macro('withCarrierAccount', ...)` adds a chainable step rather than a starting point. A macro is an ordinary closure, so it can take arguments. A carrier with separate accounts per region can be modelled as `Http::macro('carrier', fn (string $region = 'eu') => Http::baseUrl(config("services.carrier.{$region}.url"))->withToken(config("services.carrier.{$region}.token")))`, called as `Http::carrier('us')->get('rates')`. The closure is bound to the factory, so `$this->baseUrl(...)` inside it would also work; calling through the facade reads more clearly. The same pattern gives every integration one obvious place where its base URL, credentials, timeout and retry policy are decided. When the carrier changes its API version or a timeout needs tuning, one line changes instead of every call site. ## How baseUrl() joins paths `baseUrl()` stores a prefix. When the request is sent, Laravel checks the URL: if it starts with `http://` or `https://` it is used **as is**; otherwise Laravel trims the trailing slash from the base, trims leading slashes from the path, and joins them with a single `/`. | baseUrl | Path passed | URL requested | |---|---|---| | `https://carrier.example/api/v2` | `/rates` | `https://carrier.example/api/v2/rates` | | `https://carrier.example/api/v2/` | `rates` | `https://carrier.example/api/v2/rates` | | `https://carrier.example/api/v2` | `https://status.carrier.example/ping` | `https://status.carrier.example/ping` | This is plain string joining, so the base URL's path prefix is kept whether or not the path starts with a slash. ## Choosing between the options | Tool | Scope | Good for | |---|---|---| | `Http::macro()` | one named starting point | one client per third-party API | | `Http::globalOptions([...])` | every request the factory creates | an app-wide default such as a timeout | | `Http::globalRequestMiddleware()` | every outgoing request | a header every call must carry, such as a user agent | | a service class wrapping `Http` | one integration | domain methods such as `rates()` and `buyLabel()` with typed results | Global options are applied when each `PendingRequest` is created, so a later `timeout()` on the chain still overrides them. ## Pitfalls - Storing one `PendingRequest` in a long-lived service and chaining onto it: its methods **mutate and return the same object**, so a header added for one call stays for every later call. - Registering the macro somewhere that does not run for every entry point, such as inside one controller, so a queued job calling `Http::carrier()` hits a `BadMethodCallException`. - Mixing absolute and relative URLs in one integration, then wondering why a staging `baseUrl` is ignored for part of the traffic. - Returning something other than a `PendingRequest` from the macro, which breaks the fluent chain callers expect.

  • What happens if you pass an absolute URL to a request built from a baseUrl macro?
    Laravel applies `baseUrl()` only when the URL does not start with `http://` or `https://`. An absolute URL is sent unchanged, with the macro's token and headers still attached — worth remembering before sending a carrier token to another host.
  • How would you set a 5-second timeout for every outgoing request in a Laravel app?
    Call `Http::globalOptions(['timeout' => 5])` in `AppServiceProvider::boot()`. The factory applies global options when it creates each `PendingRequest`, so an explicit `timeout()` on a chain still overrides the global value.
  • Why not keep one configured PendingRequest in a singleton service and reuse it?
    `PendingRequest` methods mutate the object and return `$this`. Any header, option or retry setting one caller chains onto the shared instance stays for every later caller. A macro or a method that builds a new request per call avoids that leak.

saying these in an interview costs you the question

  • Http::withToken($t) on one line sets the token for later Http::get() calls.
  • The Http facade is a static class that keeps request settings in static properties.
  • baseUrl('https://carrier.example/api/v2') with get('/rates') drops the /api/v2 prefix.
  • baseUrl() also rewrites absolute URLs to the configured host.
  • Sharing one PendingRequest instance across callers is safe because each send() resets it.