Response Helpers#

Webrick ships ergonomic helpers to return JSON, text, XML/HTML, file downloads, redirects, and streamed output. You can also set headers/cookies, status codes, and rely on content negotiation.


Basics#

Import the helper:

use Infocyph\Webrick\Response\Response;

Return a response from any handler:

Route::get('/plain', fn () => Response::plaintext('OK', 200));
Route::get('/json',  fn () => Response::json(['ok' => true]));

Each helper returns an immutable Response; chaining methods returns a new instance.


Status codes#

Response::json(['created'=>true], 201);
Response::plaintext('Not found', 404);
Response::create('<h1>Hi</h1>', 200, ['Content-Type'=>'text/html; charset=UTF-8']);

Headers#

return Response::json(['ok'=>true])
  ->withHeader('X-Request-Id', 'abc123')
  ->withAddedHeader('Cache-Control', 'no-store');

Use withHeader() to overwrite, withAddedHeader() to append.


Cookies (response)#

Set a cookie via header (keep it explicit so attributes are clear):

$cookie = rawurlencode('demo') . '=' . rawurlencode('value') .
          '; Path=/; HttpOnly; SameSite=Lax; Secure';
return Response::json(['ok'=>true])->withAddedHeader('Set-Cookie', $cookie);

If you enabled CookieEncryptionMiddleware, the request side will automatically decrypt.


JSON#

return Response::json([
  'id'    => 42,
  'name'  => 'Hasan',
  'roles' => ['admin','ops'],
]);
  • Uses UTF-8 JSON with sane defaults.

  • For large payloads, consider pagination or streaming where appropriate.


Text#

return Response::plaintext("Hello world\n", 200);

XML / HTML#

$xml = '<note><to>World</to><msg>Hello</msg></note>';
return Response::create($xml, 200, ['Content-Type' => 'application/xml']);
$html = '<!doctype html><title>OK</title><h1>It works</h1>';
return Response::create($html, 200, ['Content-Type' => 'text/html; charset=UTF-8']);

Redirects#

return Response::redirect('/login', 302);

// With named route
$url = Route::urlFor('profile.show', ['id'=>7]);
return Response::redirect($url, 302);

Absolute vs relative is supported by Route::urlFor(..., absolute:true).


File downloads#

$path = __DIR__ . '/../storage/report.csv';
return Response::attachment($path, 'report.csv');  // sets headers & streams file
  • Validates the file path and sets Content-Disposition.

  • Prefer streaming (the helper does) for large files.


Streaming responses#

Emit chunked output without buffering the whole body:

return Response::stream(function () {
  for ($i=1; $i<=5; $i++) {
    yield "chunk: {$i}\n";
    usleep(120_000);
  }
  return ''; // optional last chunk
});

Tips:

  • Great for server-sent events, logs, progress output.

  • Pair with appropriate web-server settings to avoid buffering.


Content negotiation: Response::auto()#

Use client headers to decide JSON/text/XML automatically:

Route::get('/auto-demo', function ($r) {
  $data = ['msg'=>'hello','time'=>time()];
  return Response::auto($r, $data);
});
  • Looks at Accept and (optionally) request attributes injected by negotiation middleware.

  • Keeps your handlers simple when multiple formats are acceptable.


ETags & cache validators (behavioral)#

When CacheValidatorsMiddleware is active, responses can carry ETag / Last-Modified, and the middleware will short-circuit 304 Not Modified / 412 Precondition Failed when client validators match. Combine with compression (post-global) for optimal wire bytes.

You generally don’t set ETag manually; the middleware coordinates it (and with compression).


CORS & security headers#

Add CORS/Policy headers in a post-global middleware (recommended):

  • Access-Control-Allow-Origin, Access-Control-Allow-Headers, etc.

  • X-Content-Type-Options: nosniff, Referrer-Policy: ..., Permissions-Policy: ...

Keep policy decisions centralized, not per-handler.


Error responses#

return Response::json(['error' => 'Invalid input'], 422)
  ->withHeader('X-Error-Code', 'E_INPUT');

For generic 500s, prefer a global exception handler that transforms throwables into structured responses (and logs).


Custom Status Codes#

Use any valid HTTP status code:

// 201 Created (with Location header)
$id = 42;
$url = Route::urlFor('users.show', ['id' => $id], absolute: true);
return Response::json(['id' => $id], 201)
    ->withHeader('Location', $url);

// 204 No Content (empty body for DELETE)
return Response::create('', 204);

// 206 Partial Content (for Range requests - handled by middleware usually)
return Response::create($partialContent, 206, [
    'Content-Range' => "bytes 0-1023/4096",
    'Content-Length' => '1024'
]);

// 304 Not Modified (handled by CacheValidatorsMiddleware usually)
return Response::create('', 304)
    ->withHeader('ETag', $etag);

// 401 Unauthorized (with WWW-Authenticate)
return Response::json(['error' => 'Authentication required'], 401)
    ->withHeader('WWW-Authenticate', 'Bearer realm="api"');

// 403 Forbidden
return Response::json(['error' => 'Insufficient permissions'], 403);

// 405 Method Not Allowed (with Allow header)
return Response::json(['error' => 'Method not allowed'], 405)
    ->withHeader('Allow', 'GET, POST');

// 406 Not Acceptable
return Response::json(['error' => 'Cannot produce requested format'], 406);

// 410 Gone (for expired/deleted resources)
return Response::json(['error' => 'Resource no longer available'], 410);

// 418 I'm a teapot (RFC 2324 - Easter egg)
return Response::plaintext("I'm a teapot", 418);

// 422 Unprocessable Entity (validation errors)
return Response::json([
    'error' => 'Validation failed',
    'errors' => [
        'email' => ['Email is required', 'Email must be valid'],
        'password' => ['Password must be at least 8 characters']
    ]
], 422);

// 429 Too Many Requests (handled by ThrottleMiddleware usually)
return Response::json(['error' => 'Rate limit exceeded'], 429)
    ->withHeader('Retry-After', '60');

// 451 Unavailable For Legal Reasons
return Response::json([
    'error' => 'This content is not available in your region'
], 451);

// 503 Service Unavailable (maintenance mode)
return Response::json([
    'error' => 'Service temporarily unavailable',
    'retry_after' => 300
], 503)->withHeader('Retry-After', '300');

Status Code Quick Reference#

Code

Meaning

Use Case

200

OK

Standard success

201

Created

Resource created (POST)

202

Accepted

Async processing started

204

No Content

Success, no body (DELETE)

301

Moved Permanently

Resource relocated forever

302

Found

Temporary redirect

303

See Other

Redirect after POST

304

Not Modified

Cached version still valid

307

Temporary Redirect

Preserve method on redirect

308

Permanent Redirect

Preserve method, permanent

400

Bad Request

Invalid syntax/parameters

401

Unauthorized

Authentication required

403

Forbidden

Authenticated but not authorized

404

Not Found

Resource doesn’t exist

405

Method Not Allowed

Wrong HTTP verb

406

Not Acceptable

Can’t satisfy Accept header

408

Request Timeout

Client too slow

409

Conflict

Resource state conflict

410

Gone

Resource permanently deleted

413

Payload Too Large

Body exceeds limit

415

Unsupported Media Type

Wrong Content-Type

422

Unprocessable Entity

Validation failed

429

Too Many Requests

Rate limit exceeded

451

Unavailable For Legal

Censored/blocked

500

Internal Server Error

Unhandled exception

502

Bad Gateway

Upstream error

503

Service Unavailable

Maintenance/overload

504

Gateway Timeout

Upstream timeout


Examples#

Conditional attachment vs inline#

Route::get('/report', function ($r) {
  $path = __DIR__.'/../reports/today.csv';
  if ($r->query('dl')) {
    return Response::attachment($path, 'today.csv');
  }
  return Response::create(file_get_contents($path), 200, [
    'Content-Type' => 'text/csv; charset=UTF-8',
  ]);
});

Checklist#

  • Choose the right helper (json, plaintext, create, redirect, attachment, stream)

  • Set explicit content types for HTML/XML/custom formats

  • Add headers immutably (withHeader, withAddedHeader)

  • Prefer streaming for large or long-running responses

  • Let middleware manage ETags/compression/CORS consistently