@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-ZOEPKBVF.js → chunk-4MWG3B7G.js} +1347 -428
- package/dist/chunk-4MWG3B7G.js.map +1 -0
- package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
- package/dist/chunk-7ZQZYDOC.js.map +1 -0
- package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
- package/dist/chunk-GVNLZZTN.js.map +1 -0
- package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
- package/dist/chunk-VOYW3R5J.js.map +1 -0
- package/dist/contracts.d.ts +515 -17
- package/dist/contracts.js +1 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +964 -152
- package/dist/index.js +4279 -866
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
- package/dist/jsx-runtime.d.ts +2 -2
- package/dist/jsx-runtime.js +3 -3
- package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -3
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +651 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +276 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +209 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +384 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +273 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +396 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +322 -0
- package/package.json +34 -32
- package/dist/chunk-I63ZFMX6.js.map +0 -1
- package/dist/chunk-MFTZQMZM.js.map +0 -1
- package/dist/chunk-POEP4B2M.js.map +0 -1
- package/dist/chunk-ZOEPKBVF.js.map +0 -1
- package/dist/index.d-BJyJx6U1.d.ts +0 -319
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# HTTP responses
|
|
2
|
+
|
|
3
|
+
Whatever a route or a controller returns becomes the response: text, JSON, a view, a redirect, a download or a
|
|
4
|
+
`Response` you build yourself.
|
|
5
|
+
|
|
6
|
+
## Creating responses
|
|
7
|
+
|
|
8
|
+
#### Strings and arrays
|
|
9
|
+
|
|
10
|
+
A route returns its response. A string is sent as HTML, with a `Content-Type` of `text/html; charset=utf-8`:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
Route.get('/', () => 'Hello World')
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A number or a boolean is sent the same way, as its text. An array or a plain object is sent as JSON, with a
|
|
17
|
+
`Content-Type` of `application/json`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
Route.get('/', () => [1, 2, 3])
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`null` and `undefined` send an empty 204 response, so an action that returns nothing answers "No Content".
|
|
24
|
+
|
|
25
|
+
#### Response objects
|
|
26
|
+
|
|
27
|
+
`response()` with content builds a `Response` with the status and the headers you give it. The status is 200
|
|
28
|
+
unless you give one, and text is HTML unless you name another `Content-Type`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
Route.get('/home', () => response('Hello World', 200, { 'Content-Type': 'text/plain' }))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> [!NOTE]
|
|
35
|
+
> Laravel's `Response` extends Symfony's response class, with chainable methods such as `header()` and `cookie()`.
|
|
36
|
+
> Oriel uses the web-standard `Response` built into Node and Bun, and its server sends it as it is, so a route can
|
|
37
|
+
> also return one from a library or from `fetch()`. Its headers are set when you build it, or through its `headers`
|
|
38
|
+
> object, as below.
|
|
39
|
+
|
|
40
|
+
#### Eloquent models and collections
|
|
41
|
+
|
|
42
|
+
A model, a collection of models or a paginator is sent as JSON. A model leaves out its hidden attributes, such as
|
|
43
|
+
the `password` and `remember_token` of the `User` model:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
Route.get('/user/{user}', (user: User) => user)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Views
|
|
50
|
+
|
|
51
|
+
A view returned from a route is rendered and sent as HTML:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
Route.get('/', () => view('welcome', { name: 'Taylor' }))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
[Views](views.md) covers writing them.
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> Return a view, not a TSX element. A route that returns `<Welcome />` sends the element's description as JSON. To
|
|
61
|
+
> send TSX that isn't a view file, render it first: `response(await View.renderToString(<Welcome />))`.
|
|
62
|
+
|
|
63
|
+
### Attaching headers to responses
|
|
64
|
+
|
|
65
|
+
Give the headers as `response()`'s third argument, as an object or as a list of pairs:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
declare const content: string
|
|
69
|
+
declare const type: string
|
|
70
|
+
// ---cut---
|
|
71
|
+
const res = response(content, 200, {
|
|
72
|
+
'Content-Type': type,
|
|
73
|
+
'X-Header-One': 'Header Value',
|
|
74
|
+
'X-Header-Two': 'Header Value',
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A `Response`'s `headers` object adds, replaces and removes headers after it is built:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
declare const content: string
|
|
82
|
+
// ---cut---
|
|
83
|
+
const res = response(content)
|
|
84
|
+
|
|
85
|
+
res.headers.set('X-Header-One', 'Header Value')
|
|
86
|
+
res.headers.append('Vary', 'Accept')
|
|
87
|
+
res.headers.delete('X-Debug')
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
On Node, the headers of a response from `fetch()` or `Response.redirect()` can't be changed; Bun allows it. A
|
|
91
|
+
middleware, which may receive such a response, changes the headers with `withHeaders()`, which returns a copy whose
|
|
92
|
+
headers can be changed on both:
|
|
93
|
+
|
|
94
|
+
```ts title="app/Http/Middleware/AddFrameOptions.ts"
|
|
95
|
+
import { withHeaders } from '@orieljs/framework'
|
|
96
|
+
import type { HttpRequest, Middleware, Next } from '@orieljs/framework/contracts'
|
|
97
|
+
|
|
98
|
+
export class AddFrameOptions implements Middleware {
|
|
99
|
+
async handle(request: HttpRequest, next: Next): Promise<Response> {
|
|
100
|
+
const response = await next(request)
|
|
101
|
+
|
|
102
|
+
return withHeaders(response, (headers) => {
|
|
103
|
+
headers.set('X-Frame-Options', 'DENY')
|
|
104
|
+
headers.delete('X-Powered-By')
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Oriel has no `cache.headers` middleware: set `Cache-Control` as any other header.
|
|
111
|
+
|
|
112
|
+
### Attaching cookies to responses
|
|
113
|
+
|
|
114
|
+
Queue a cookie with the `Cookie` facade, and it is added to the response when the response is ready. `cookie()`
|
|
115
|
+
makes the cookie, from its name, its value and the number of minutes it lasts:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
Route.get('/', () => {
|
|
119
|
+
Cookie.queue(cookie('name', 'value', 60))
|
|
120
|
+
|
|
121
|
+
return 'Hello World'
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Queued cookies are added by the `AddQueuedCookiesToResponse` middleware, in the `web` group. A route in
|
|
126
|
+
`routes/api.ts` doesn't send them.
|
|
127
|
+
|
|
128
|
+
A redirect takes a cookie of its own with `withCookie()`, in any group:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const res = redirect('/dashboard').withCookie('name', 'value', 60)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> [!NOTE]
|
|
135
|
+
> Laravel attaches a cookie to a response with its `cookie()` method, and `Cookie::queue()` takes the cookie's name,
|
|
136
|
+
> value and minutes. A web-standard `Response` has no such method, so Oriel queues a cookie that `cookie()` made:
|
|
137
|
+
> `Cookie.queue(cookie('name', 'value', 60))`.
|
|
138
|
+
|
|
139
|
+
#### Generating cookie instances
|
|
140
|
+
|
|
141
|
+
`cookie()` returns a plain cookie, which isn't sent until you queue it. Its fourth argument changes the cookie's
|
|
142
|
+
`path`, `domain`, `secure`, `httpOnly` and `sameSite`:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const theme = cookie('theme', 'dark', 60 * 24 * 30, { httpOnly: false, sameSite: 'strict' })
|
|
146
|
+
|
|
147
|
+
Cookie.queue(theme)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A cookie is `HttpOnly` unless you say otherwise, and takes its path, domain, `secure` and `sameSite` from
|
|
151
|
+
`config/session.ts`. With 0 minutes, the default, it lasts until the browser closes. `Cookie.forever('name', 'value')`
|
|
152
|
+
makes one that lasts 400 days, and `Cookie.make()` is the same as `cookie()`.
|
|
153
|
+
|
|
154
|
+
To write a cookie onto a `Response` yourself, give its `Set-Cookie` header the text `serializeCookie()` makes:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const res = response('Hello World', 200, { 'Set-Cookie': serializeCookie(cookie('name', 'value', 60)) })
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Expiring cookies early
|
|
161
|
+
|
|
162
|
+
`Cookie.forget()` makes a cookie that has already expired, so the browser deletes it. Queue it:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
Cookie.queue(Cookie.forget('name'))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Laravel's `withoutCookie()` and `Cookie::expire()` aren't in Oriel.
|
|
169
|
+
|
|
170
|
+
### Cookies and encryption
|
|
171
|
+
|
|
172
|
+
The `EncryptCookies` middleware, in the `web` group, encrypts every cookie the response sets, however it was set:
|
|
173
|
+
queued, attached to a redirect or written as a `Set-Cookie` header. The browser can't read or change them, and
|
|
174
|
+
`request().cookie()` decrypts them on the next request (see
|
|
175
|
+
[Requests](requests.md#retrieving-cookies-from-requests)). A cookie that deletes one, as `Cookie.forget()` makes, is
|
|
176
|
+
sent as it is.
|
|
177
|
+
|
|
178
|
+
To send some cookies unencrypted, such as a cookie your scripts read, list their names with
|
|
179
|
+
`EncryptCookies.except()` in `bootstrap/app.ts`, as the starter kits do for the appearance cookie:
|
|
180
|
+
|
|
181
|
+
```ts title="bootstrap/app.ts"
|
|
182
|
+
import { dirname } from 'node:path'
|
|
183
|
+
import { Application, EncryptCookies } from '@orieljs/framework'
|
|
184
|
+
import providers from './providers'
|
|
185
|
+
|
|
186
|
+
export function createApplication(): Promise<Application> {
|
|
187
|
+
return Application.configure({ basePath: dirname(import.meta.dirname) })
|
|
188
|
+
.withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })
|
|
189
|
+
.withMiddleware(() => {
|
|
190
|
+
EncryptCookies.except(['cookie_name'])
|
|
191
|
+
})
|
|
192
|
+
.withProviders(providers)
|
|
193
|
+
.create()
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Such a cookie is also read as the browser sent it. Laravel's `$middleware->encryptCookies(except: [...])` is
|
|
198
|
+
`EncryptCookies.except([...])` in Oriel.
|
|
199
|
+
|
|
200
|
+
The `api` group has no `EncryptCookies`: an API route's cookies are sent and read as they are.
|
|
201
|
+
|
|
202
|
+
## Redirects
|
|
203
|
+
|
|
204
|
+
`redirect()` with a path returns a redirect response, a 302 to that path on the host the request came to. A second
|
|
205
|
+
argument changes the status:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
Route.get('/dashboard', () => redirect('/home/dashboard'))
|
|
209
|
+
|
|
210
|
+
Route.get('/old', () => redirect('/new', 301))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`back()` redirects to the previous page, such as when a submitted form is invalid. It reads the request's `Referer`
|
|
214
|
+
header, then the last page the session recorded, and falls back to the path you give it, or `/`:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
Route.post('/user/profile', () => {
|
|
218
|
+
// Validate the request...
|
|
219
|
+
|
|
220
|
+
return back().withInput()
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The session records each `GET` page a user visits through the `web` group, leaving out AJAX requests and prefetches.
|
|
225
|
+
|
|
226
|
+
### Redirecting to named routes
|
|
227
|
+
|
|
228
|
+
`redirect()` with no arguments returns the redirector, whose `route()` redirects to a named route:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const res = redirect().route('login')
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A route's parameters are the second argument, and the status the third:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// For a route with the following URI: /profile/{id}
|
|
238
|
+
|
|
239
|
+
const res = redirect().route('profile', { id: 1 })
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Once `route:types` has run, TypeScript checks the route's name and its parameters, as it does for `route()`: see
|
|
243
|
+
[typed route parameters](routing.md#typed-route-parameters). Laravel's `redirect()->action()`, which redirects to a
|
|
244
|
+
controller action, isn't in Oriel: name the route and redirect to its name.
|
|
245
|
+
|
|
246
|
+
#### Populating parameters via Eloquent models
|
|
247
|
+
|
|
248
|
+
A model fills a parameter with its route key, its `id` unless you change it:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
declare const user: User
|
|
252
|
+
// ---cut---
|
|
253
|
+
// For a route with the following URI: /profile/{id}
|
|
254
|
+
|
|
255
|
+
const res = redirect().route('profile', user)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A `{id:slug}` parameter, or `getRouteKeyName()` on the model, fills it from another column: see
|
|
259
|
+
[customizing the key](routing.md#customizing-the-key).
|
|
260
|
+
|
|
261
|
+
### Redirecting to external domains
|
|
262
|
+
|
|
263
|
+
`redirect()` sends a URL that starts with `http://`, `https://`, `//`, `#`, `mailto:`, `tel:` or `sms:` as it is,
|
|
264
|
+
and treats anything else as a path of your app. `away()` sends any target as it is:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const res = redirect().away('https://www.google.com')
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Redirecting with flashed session data
|
|
271
|
+
|
|
272
|
+
A redirect usually comes with a message for the next page, [flashed to the session](session.md#flash-data). `with()`
|
|
273
|
+
flashes a value, or an object of values, as it redirects:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
Route.post('/user/profile', () => {
|
|
277
|
+
// ...
|
|
278
|
+
|
|
279
|
+
return redirect('/dashboard').with('status', 'Profile updated!')
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The next page reads it from the session. In a TSX view:
|
|
284
|
+
|
|
285
|
+
```tsx title="resources/views/dashboard.tsx"
|
|
286
|
+
import { session } from '@orieljs/framework'
|
|
287
|
+
|
|
288
|
+
export default function Dashboard() {
|
|
289
|
+
const status = session<string | undefined>('status')
|
|
290
|
+
|
|
291
|
+
return <main>{status && <div class="alert alert-success">{status}</div>}</main>
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Redirecting with input
|
|
296
|
+
|
|
297
|
+
`withInput()` flashes the request's input, so the form can show it again after a failed validation. `old()` reads
|
|
298
|
+
it on the next request (see [Requests](requests.md#old-input)):
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const res = back().withInput()
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
It takes the input to flash, too: `withInput(request().except('password'))`.
|
|
305
|
+
|
|
306
|
+
#### Redirecting with errors
|
|
307
|
+
|
|
308
|
+
`withErrors()` flashes error messages, by field, for `errors()` to read on the next page. A field takes one message
|
|
309
|
+
or a list:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const res = back().withErrors({ email: 'These credentials do not match our records.' }).withInput()
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
A second argument names an error bag, so two forms on one page keep their errors apart:
|
|
316
|
+
`withErrors({ email: '…' }, 'login')`, read with `errors('login')`. A failed [validation](validation.md) does all of
|
|
317
|
+
this for you.
|
|
318
|
+
|
|
319
|
+
A redirect takes headers too: `redirect('/home').header('X-Reason', 'moved')`. `redirect().refresh()` redirects to the
|
|
320
|
+
current URL. `redirect().guest(path)` remembers the page a guest was going to before it redirects, and
|
|
321
|
+
`redirect().intended(fallback, status)` later sends the user there; `redirect().setIntendedUrl(url)` sets that page
|
|
322
|
+
yourself. The Inertia starter kits' sign-in uses `intended()`, and
|
|
323
|
+
[Authentication](authentication.md#redirecting-unauthenticated-users) covers the flow.
|
|
324
|
+
|
|
325
|
+
## Other response types
|
|
326
|
+
|
|
327
|
+
`response()` with no arguments returns the response factory, whose methods build the other kinds of response.
|
|
328
|
+
|
|
329
|
+
### View responses
|
|
330
|
+
|
|
331
|
+
To send a view with your own status or headers, use the factory's `view()`. It renders the view and returns a
|
|
332
|
+
promise of the response:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
declare const data: Record<string, unknown>
|
|
336
|
+
declare const type: string
|
|
337
|
+
// ---cut---
|
|
338
|
+
Route.get('/hello', () => response().view('hello', data, 200, { 'Content-Type': type }))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Without a status or headers of your own, return the `view()` itself.
|
|
342
|
+
|
|
343
|
+
### JSON responses
|
|
344
|
+
|
|
345
|
+
`json()` turns its data into JSON, with a `Content-Type` of `application/json`. A status and headers follow:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
const res = response().json({ name: 'Abigail', state: 'CA' })
|
|
349
|
+
|
|
350
|
+
const created = response().json({ name: 'Abigail' }, 201)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The data goes through `JSON.stringify()`, so models and collections become JSON as they do when a route returns
|
|
354
|
+
them, and dates become ISO strings. Laravel's JSONP `withCallback()` isn't in Oriel.
|
|
355
|
+
|
|
356
|
+
### Empty responses
|
|
357
|
+
|
|
358
|
+
`noContent()` sends an empty 204 response, or another status you give it:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
const res = response().noContent()
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### File downloads
|
|
365
|
+
|
|
366
|
+
`download()` sends a file for the browser to save. A second argument is the name the user sees, and a third the
|
|
367
|
+
headers:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
declare const pathToFile: string
|
|
371
|
+
declare const name: string
|
|
372
|
+
declare const headers: Record<string, string>
|
|
373
|
+
// ---cut---
|
|
374
|
+
Route.get('/report', () => response().download(pathToFile))
|
|
375
|
+
|
|
376
|
+
Route.get('/report/named', () => response().download(pathToFile, name, headers))
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The file is streamed from disk, with its size, and a `Content-Type` from its name's extension unless your headers
|
|
380
|
+
give one. A name that isn't ASCII is sent too, so `Résumé.pdf` arrives as it is. A file that doesn't exist is a 404.
|
|
381
|
+
`download()` returns a promise: return it from the route, or await it.
|
|
382
|
+
|
|
383
|
+
### File responses
|
|
384
|
+
|
|
385
|
+
`file()` sends a file for the browser to show, such as an image or a PDF, rather than to download. It streams the
|
|
386
|
+
file as `download()` does, with a `Content-Type` from its extension unless your headers give one:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
declare const pathToFile: string
|
|
390
|
+
declare const headers: Record<string, string>
|
|
391
|
+
// ---cut---
|
|
392
|
+
Route.get('/invoice', () => response().file(pathToFile))
|
|
393
|
+
|
|
394
|
+
Route.get('/invoice/raw', () => response().file(pathToFile, headers))
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Streamed responses
|
|
398
|
+
|
|
399
|
+
`stream()` sends a web `ReadableStream`, chunk by chunk, as your code produces them. The client can start reading
|
|
400
|
+
before the response ends:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
Route.get('/stream', () => {
|
|
404
|
+
const encoder = new TextEncoder()
|
|
405
|
+
|
|
406
|
+
const body = new ReadableStream<Uint8Array>({
|
|
407
|
+
async start(controller) {
|
|
408
|
+
for (const text of ['developer', 'admin']) {
|
|
409
|
+
controller.enqueue(encoder.encode(text))
|
|
410
|
+
await new Promise((resolve) => setTimeout(resolve, 2000)) // Simulate delay between chunks...
|
|
411
|
+
}
|
|
412
|
+
controller.close()
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
return response().stream(body, 200, { 'X-Accel-Buffering': 'no' })
|
|
417
|
+
})
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
> [!NOTE]
|
|
421
|
+
> Laravel's `stream()` takes a closure that echoes its output and flushes PHP's buffers. Node and Bun write a web
|
|
422
|
+
> stream's chunks as they come, with no output buffer, so Oriel's `stream()` takes the stream itself, from your code
|
|
423
|
+
> or from a library that returns one.
|
|
424
|
+
|
|
425
|
+
### Streamed downloads
|
|
426
|
+
|
|
427
|
+
A stream becomes a download with a `Content-Disposition` header. `contentDisposition()` writes it, with the name
|
|
428
|
+
encoded as `download()` encodes it:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
declare const body: ReadableStream<Uint8Array>
|
|
432
|
+
// ---cut---
|
|
433
|
+
const res = response().stream(body, 200, {
|
|
434
|
+
'Content-Type': 'text/markdown',
|
|
435
|
+
'Content-Disposition': contentDisposition('attachment', 'readme.md'),
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`contentDisposition('inline', 'invoice.pdf')` asks the browser to show the file instead. Laravel's `streamJson()`
|
|
440
|
+
and `eventStream()` aren't in Oriel.
|
|
441
|
+
|
|
442
|
+
## Response macros
|
|
443
|
+
|
|
444
|
+
Oriel has no response macros. A response you reuse is a function of your own that returns a `Response`.
|