In Laravel's HTTP client, what body does Http::post() send by default, and how do asForm(), attach(), withToken() and acceptJson() change the request?
answer
- JSON unless told otherwise
- asForm: x-www-form-urlencoded
- attach switches to multipart
- withToken defaults to Bearer
- acceptJson sets Accept only
basics
~20 sHttp::post() encodes its array as a JSON body with Content-Type application/json. asForm() sends URL-encoded form fields, attach() switches to multipart with a file part, withToken() adds an Authorization: Bearer header, and acceptJson() sets only the Accept header.
solid answer
~40 sEvery `PendingRequest` starts in JSON mode, so `Http::post($url, $data)` JSON-encodes the array and sends `Content-Type: application/json`; you never call `json_encode()` yourself. `asForm()` switches the body to `application/x-www-form-urlencoded`, which OAuth token endpoints usually require. `attach('invoice', $contents, 'invoice.pdf')` switches to a multipart body and adds a file part; other fields passed to `post()` travel as ordinary parts. `withToken($token)` sets `Authorization: Bearer <token>` (the second argument changes the scheme), `withHeaders([...])` merges extra headers, and `acceptJson()` only sets `Accept: application/json` — it asks for a JSON response and does not change the request body. For `get()`, the second argument becomes the query string rather than a body.
code
php · 14 lines<?php
use Illuminate\Support\Facades\Http;
// multipart: one file part plus ordinary fields
$label = Http::withToken($token)
->acceptJson()
->attach('invoice', file_get_contents($pdfPath), 'invoice.pdf')
->post('https://carrier.example/api/shipments', [
'service' => 'express',
'reference' => 'ORDER-1042',
])
->throw()
->json('label_url');go deeper
Remember that post() sends JSON by default, asForm() sends form fields, attach() sends multipart, and withToken() adds a Bearer header.
Explain which Guzzle option each body method selects, why acceptJson() affects only the response, and how get() turns its array into a query string.
Show you debug integration failures from the wire format: the Content-Type sent, double-encoded payloads, and headers merged where you meant to replace them.
Argue for wrapping each third-party API in one place so body format, auth scheme and headers are decided once rather than per call site.
## The default: a JSON body Every `Illuminate\Http\Client\PendingRequest` is created in JSON mode: its constructor calls `asJson()`, which sets the **body format** to Guzzle's `json` option and the `Content-Type` header to `application/json`. So `Http::post($url, ['service' => 'express'])` encodes the array as JSON for you, and `put()`, `patch()` and `delete()` follow the same rule. `get()` and `head()` are different: their second argument is the **query string**, so `Http::get($url, ['zip' => '10115'])` requests `...?zip=10115`. Use `withQueryParameters([...])` to add parameters to every request sent from the same chain, and `withUrlParameters([...])` to fill URI-template placeholders in the URL: ```php Http::withUrlParameters(['parcel' => '1Z999', 'lang' => 'en']) ->get('https://carrier.example/api/tracking/{parcel}?lang={lang}'); ``` Laravel expands the template before sending, so values are URL-encoded for you instead of being concatenated into the string by hand. Two words that sound alike do different jobs here. The **body format** decides how the array you pass becomes bytes on the wire and which `Content-Type` announces it. **Headers** such as `Authorization` and `Accept` travel alongside the body and do not change its encoding. Most integration bugs in this area come from confusing the two. ## Changing the body format | Method | Guzzle option used | Content-Type sent | |---|---|---| | default, or `asJson()` | `json` | `application/json` | | `asForm()` | `form_params` | `application/x-www-form-urlencoded` | | `attach()` or `asMultipart()` | `multipart` | `multipart/form-data` with a boundary | | `withBody($content, $type)` | `body` | the type you pass, `application/json` if omitted | - **`asForm()`** is what OAuth token endpoints and older form-style APIs expect. Sending them JSON is the classic "the fields are right but I get a 400" bug. - **`attach($name, $contents, $filename = null, $headers = [])`** switches the request to multipart and adds a file part. `$contents` may be a string or a stream resource; pass an array of such argument lists to attach several files. An array passed to `post()` alongside it becomes ordinary multipart fields. - **`withBody()`** sends a raw string, for XML or a payload you have already serialised and signed. ## Headers and authentication - `withHeaders(['X-Carrier-Account' => '42'])` **merges** headers into the request; it does not reset earlier ones. Use `replaceHeaders()` when a value must be overwritten. - `withHeader($name, $value)` adds a single header. - `withToken($token)` sets `Authorization: Bearer <token>`; the optional second argument changes the scheme, as in `withToken($key, 'Token')`. - `withBasicAuth($user, $password)` and `withDigestAuth($user, $password)` cover the other common schemes. - `accept($type)` sets the `Accept` header, and **`acceptJson()`** is shorthand for `accept('application/json')`. `acceptJson()` is often misunderstood. It tells the server which **response** format you want; it does not touch the request body, which is already JSON by default. Many frameworks, Laravel included, pick between an HTML and a JSON error page based on that header, so it is worth sending whenever you plan to call `$response->json()`. ## A shipping-carrier example A carrier integration typically needs a form-encoded token request followed by authenticated calls: ```php $token = Http::asForm()->post($tokenUrl, [ 'grant_type' => 'client_credentials', 'client_id' => config('services.carrier.id'), 'client_secret' => config('services.carrier.secret'), ])->throw()->json('access_token'); $rates = Http::withToken($token)->acceptJson() ->get($ratesUrl, ['zip' => '10115']) ->json('rates'); ``` The first call sends form fields, the second a bearer token and a query string. ## Picking the method for an endpoint | The carrier's documentation says | Chain this | |---|---| | "POST a JSON object" | nothing — the default already does it | | "form parameters" or "x-www-form-urlencoded" | `asForm()` | | "upload a file" or "multipart/form-data" | `attach()` for each file | | "raw XML body" | `withBody($xml, 'application/xml')` | | "Bearer token in the Authorization header" | `withToken($token)` | | "API key in the X-Api-Key header" | `withHeaders(['X-Api-Key' => $key])` | Reading the wire format from the documentation first, then choosing one method per requirement, avoids most of the trial-and-error that integrations otherwise go through. ## Common mistakes 1. Calling `json_encode()` on the payload before `post()`, which sends a JSON **string** encoded a second time. 2. Chaining `acceptJson()` and expecting the request body to change. 3. Forgetting `asForm()` for an endpoint that reads only form fields. 4. Putting query parameters in `post()`'s array and expecting them in the URL — for a POST that array is the body. 5. Chaining `asForm()` after `attach()`: the last body-format call wins, and file parts only travel in a multipart body.
- Why does an OAuth token endpoint often reject Http::post() with a 400 even though the field names are right?Laravel sends a JSON body by default, while most OAuth token endpoints read only `application/x-www-form-urlencoded` fields. Chain `asForm()` before `post()` so the same array is sent as form parameters.
- How do you add a query string to a POST request with Laravel's HTTP client?Put the parameters in the URL or use `withQueryParameters(['dry_run' => 1])`; the array passed to `post()` is always the body. `withQueryParameters()` merges into the query of every request sent from that chain.
saying these in an interview costs you the question
- Http::post() sends form-encoded fields by default, like an HTML form.
- acceptJson() is what makes the request body JSON.
- You must json_encode() the array yourself before passing it to post().
- withToken() puts the token in a query parameter.
- Calling withHeaders() a second time discards the headers set earlier.