@alepha/react 0.9.3 → 0.9.4

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.
Files changed (37) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +315 -320
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +496 -457
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +276 -258
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +274 -256
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +494 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +13 -10
  13. package/src/components/NestedView.tsx +15 -13
  14. package/src/components/NotFound.tsx +1 -1
  15. package/src/descriptors/$page.ts +16 -4
  16. package/src/errors/Redirection.ts +8 -5
  17. package/src/hooks/useActive.ts +25 -34
  18. package/src/hooks/useAlepha.ts +16 -2
  19. package/src/hooks/useClient.ts +7 -4
  20. package/src/hooks/useInject.ts +4 -1
  21. package/src/hooks/useQueryParams.ts +9 -6
  22. package/src/hooks/useRouter.ts +18 -31
  23. package/src/hooks/useRouterEvents.ts +7 -7
  24. package/src/hooks/useRouterState.ts +8 -20
  25. package/src/hooks/useSchema.ts +10 -15
  26. package/src/hooks/useStore.ts +0 -7
  27. package/src/index.browser.ts +11 -11
  28. package/src/index.shared.ts +2 -3
  29. package/src/index.ts +21 -30
  30. package/src/providers/ReactBrowserProvider.ts +149 -65
  31. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  32. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
  33. package/src/providers/ReactServerProvider.ts +69 -74
  34. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
  35. package/src/contexts/RouterContext.ts +0 -14
  36. package/src/providers/BrowserRouterProvider.ts +0 -155
  37. package/src/providers/ReactBrowserRenderer.ts +0 -93
@@ -1,20 +1,11 @@
1
- import {
2
- $env,
3
- $hook,
4
- $inject,
5
- $logger,
6
- Alepha,
7
- type Static,
8
- t,
9
- } from "@alepha/core";
10
- import type { ApiLinksResponse } from "@alepha/server";
1
+ import { $env, $hook, $inject, Alepha, type Static, t } from "@alepha/core";
2
+ import { $logger } from "@alepha/logger";
11
3
  import { createElement, type ReactNode, StrictMode } from "react";
12
4
  import ClientOnly from "../components/ClientOnly.tsx";
13
5
  import ErrorViewer from "../components/ErrorViewer.tsx";
14
6
  import NestedView from "../components/NestedView.tsx";
15
7
  import NotFoundPage from "../components/NotFound.tsx";
16
8
  import { AlephaContext } from "../contexts/AlephaContext.ts";
17
- import { RouterContext } from "../contexts/RouterContext.ts";
18
9
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
19
10
  import {
20
11
  $page,
@@ -23,7 +14,6 @@ import {
23
14
  type PageDescriptorOptions,
24
15
  } from "../descriptors/$page.ts";
25
16
  import { Redirection } from "../errors/Redirection.ts";
26
- import type { HrefLike } from "../hooks/RouterHookApi.ts";
27
17
 
28
18
  const envSchema = t.object({
29
19
  REACT_STRICT_MODE: t.boolean({ default: true }),
@@ -33,7 +23,7 @@ declare module "@alepha/core" {
33
23
  export interface Env extends Partial<Static<typeof envSchema>> {}
34
24
  }
35
25
 
36
- export class PageDescriptorProvider {
26
+ export class ReactPageProvider {
37
27
  protected readonly log = $logger();
38
28
  protected readonly env = $env(envSchema);
39
29
  protected readonly alepha = $inject(Alepha);
@@ -86,29 +76,20 @@ export class PageDescriptorProvider {
86
76
 
87
77
  public url(
88
78
  name: string,
89
- options: { params?: Record<string, string>; base?: string } = {},
79
+ options: { params?: Record<string, string>; host?: string } = {},
90
80
  ): URL {
91
81
  return new URL(
92
82
  this.pathname(name, options),
93
83
  // use provided base or default to http://localhost
94
- options.base ?? `http://localhost`,
84
+ options.host ?? `http://localhost`,
95
85
  );
96
86
  }
97
87
 
98
- public root(state: RouterState, context: PageReactContext): ReactNode {
88
+ public root(state: ReactRouterState): ReactNode {
99
89
  const root = createElement(
100
90
  AlephaContext.Provider,
101
91
  { value: this.alepha },
102
- createElement(
103
- RouterContext.Provider,
104
- {
105
- value: {
106
- state,
107
- context,
108
- },
109
- },
110
- createElement(NestedView, {}, state.layers[0]?.element),
111
- ),
92
+ createElement(NestedView, {}, state.layers[0]?.element),
112
93
  );
113
94
 
114
95
  if (this.env.REACT_STRICT_MODE) {
@@ -118,15 +99,18 @@ export class PageDescriptorProvider {
118
99
  return root;
119
100
  }
120
101
 
102
+ /**
103
+ * Create a new RouterState based on a given route and request.
104
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
105
+ * It also handles errors and redirects.
106
+ */
121
107
  public async createLayers(
122
108
  route: PageRoute,
123
- request: PageRequest,
109
+ state: ReactRouterState,
110
+ previous: PreviousLayerData[] = [],
124
111
  ): Promise<CreateLayersResult> {
125
- const { pathname, search } = request.url;
126
- const layers: Layer[] = []; // result layers
127
112
  let context: Record<string, any> = {}; // all props
128
113
  const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
129
- request.onError = (error) => this.renderError(error); // error handler
130
114
 
131
115
  let parent = route.parent;
132
116
  while (parent) {
@@ -143,7 +127,7 @@ export class PageDescriptorProvider {
143
127
 
144
128
  try {
145
129
  config.query = route.schema?.query
146
- ? this.alepha.parse(route.schema.query, request.query)
130
+ ? this.alepha.parse(route.schema.query, state.query)
147
131
  : {};
148
132
  } catch (e) {
149
133
  it.error = e as Error;
@@ -152,7 +136,7 @@ export class PageDescriptorProvider {
152
136
 
153
137
  try {
154
138
  config.params = route.schema?.params
155
- ? this.alepha.parse(route.schema.params, request.params)
139
+ ? this.alepha.parse(route.schema.params, state.params)
156
140
  : {};
157
141
  } catch (e) {
158
142
  it.error = e as Error;
@@ -165,7 +149,6 @@ export class PageDescriptorProvider {
165
149
  };
166
150
 
167
151
  // check if previous layer is the same, reuse if possible
168
- const previous = request.previous;
169
152
  if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
170
153
  const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
171
154
 
@@ -203,7 +186,7 @@ export class PageDescriptorProvider {
203
186
  try {
204
187
  const props =
205
188
  (await route.resolve?.({
206
- ...request, // request
189
+ ...state, // request
207
190
  ...config, // params, query
208
191
  ...context, // previous props
209
192
  } as any)) ?? {};
@@ -221,13 +204,12 @@ export class PageDescriptorProvider {
221
204
  } catch (e) {
222
205
  // check if we need to redirect
223
206
  if (e instanceof Redirection) {
224
- return this.createRedirectionLayer(e.page, {
225
- pathname,
226
- search,
227
- });
207
+ return {
208
+ redirect: e.redirect,
209
+ };
228
210
  }
229
211
 
230
- this.log.error(e);
212
+ this.log.error("Page resolver has failed", e);
231
213
 
232
214
  it.error = e as Error;
233
215
  break;
@@ -249,8 +231,8 @@ export class PageDescriptorProvider {
249
231
  const path = acc.replace(/\/+/, "/");
250
232
  const localErrorHandler = this.getErrorHandler(it.route);
251
233
  if (localErrorHandler) {
252
- const onErrorParent = request.onError;
253
- request.onError = (error, context) => {
234
+ const onErrorParent = state.onError;
235
+ state.onError = (error, context) => {
254
236
  const result = localErrorHandler(error, context);
255
237
  // if nothing happen, call the parent
256
238
  if (result === undefined) {
@@ -260,28 +242,51 @@ export class PageDescriptorProvider {
260
242
  };
261
243
  }
262
244
 
245
+ // normal use case
246
+ if (!it.error) {
247
+ try {
248
+ const element = await this.createElement(it.route, {
249
+ ...props,
250
+ ...context,
251
+ });
252
+
253
+ state.layers.push({
254
+ name: it.route.name,
255
+ props,
256
+ part: it.route.path,
257
+ config: it.config,
258
+ element: this.renderView(i + 1, path, element, it.route),
259
+ index: i + 1,
260
+ path,
261
+ route: it.route,
262
+ cache: it.cache,
263
+ });
264
+ } catch (e) {
265
+ it.error = e as Error;
266
+ }
267
+ }
268
+
263
269
  // handler has thrown an error, render an error view
264
270
  if (it.error) {
265
271
  try {
266
272
  let element: ReactNode | Redirection | undefined =
267
- await request.onError(it.error, request);
273
+ await state.onError(it.error, state);
268
274
 
269
275
  if (element === undefined) {
270
276
  throw it.error;
271
277
  }
272
278
 
273
279
  if (element instanceof Redirection) {
274
- return this.createRedirectionLayer(element.page, {
275
- pathname,
276
- search,
277
- });
280
+ return {
281
+ redirect: element.redirect,
282
+ };
278
283
  }
279
284
 
280
285
  if (element === null) {
281
286
  element = this.renderError(it.error);
282
287
  }
283
288
 
284
- layers.push({
289
+ state.layers.push({
285
290
  props,
286
291
  error: it.error,
287
292
  name: it.route.name,
@@ -295,47 +300,21 @@ export class PageDescriptorProvider {
295
300
  break;
296
301
  } catch (e) {
297
302
  if (e instanceof Redirection) {
298
- return this.createRedirectionLayer(e.page, { pathname, search });
303
+ return {
304
+ redirect: e.redirect,
305
+ };
299
306
  }
300
307
  throw e;
301
308
  }
302
309
  }
303
-
304
- // normal use case
305
-
306
- const element = await this.createElement(it.route, {
307
- ...props,
308
- ...context,
309
- });
310
-
311
- layers.push({
312
- name: it.route.name,
313
- props,
314
- part: it.route.path,
315
- config: it.config,
316
- element: this.renderView(i + 1, path, element, it.route),
317
- index: i + 1,
318
- path,
319
- route: it.route,
320
- cache: it.cache,
321
- });
322
310
  }
323
311
 
324
- return { layers, pathname, search };
312
+ return { state };
325
313
  }
326
314
 
327
- protected createRedirectionLayer(
328
- href: HrefLike,
329
- context: {
330
- pathname: string;
331
- search: string;
332
- },
333
- ) {
315
+ protected createRedirectionLayer(redirect: string): CreateLayersResult {
334
316
  return {
335
- layers: [],
336
- redirect: typeof href === "string" ? href : this.href(href),
337
- pathname: context.pathname,
338
- search: context.search,
317
+ redirect,
339
318
  };
340
319
  }
341
320
 
@@ -601,16 +580,31 @@ export interface AnchorProps {
601
580
  onClick: (ev?: any) => any;
602
581
  }
603
582
 
604
- export interface RouterState {
605
- pathname: string;
606
- search: string;
583
+ export interface ReactRouterState {
584
+ /**
585
+ * Stack of layers for the current page.
586
+ */
607
587
  layers: Array<Layer>;
608
- }
609
588
 
610
- export interface TransitionOptions {
611
- state?: RouterState;
612
- previous?: PreviousLayerData[];
613
- context?: PageReactContext;
589
+ /**
590
+ * URL of the current page.
591
+ */
592
+ url: URL;
593
+
594
+ /**
595
+ * Error handler for the current page.
596
+ */
597
+ onError: ErrorHandler;
598
+
599
+ /**
600
+ * Params extracted from the URL for the current page.
601
+ */
602
+ params: Record<string, any>;
603
+
604
+ /**
605
+ * Query parameters extracted from the URL for the current page.
606
+ */
607
+ query: Record<string, string>;
614
608
  }
615
609
 
616
610
  export interface RouterStackItem {
@@ -621,33 +615,11 @@ export interface RouterStackItem {
621
615
  cache?: boolean;
622
616
  }
623
617
 
624
- export interface RouterRenderResult {
625
- state: RouterState;
626
- context: PageReactContext;
627
- redirect?: string;
628
- }
629
-
630
- export interface PageRequest extends PageReactContext {
631
- params: Record<string, any>;
632
- query: Record<string, string>;
633
-
634
- // previous layers (browser history or browser hydration, always null on server)
618
+ export interface TransitionOptions {
635
619
  previous?: PreviousLayerData[];
636
620
  }
637
621
 
638
- export interface CreateLayersResult extends RouterState {
622
+ export interface CreateLayersResult {
639
623
  redirect?: string;
640
- }
641
-
642
- /**
643
- * It's like RouterState, but publicly available in React context.
644
- * This is where we store all plugin data!
645
- */
646
- export interface PageReactContext {
647
- url: URL;
648
- onError: ErrorHandler;
649
- links?: ApiLinksResponse;
650
-
651
- params: Record<string, any>;
652
- query: Record<string, string>;
624
+ state?: ReactRouterState;
653
625
  }
@@ -4,13 +4,13 @@ import {
4
4
  $env,
5
5
  $hook,
6
6
  $inject,
7
- $logger,
8
7
  Alepha,
8
+ AlephaError,
9
9
  type Static,
10
10
  t,
11
11
  } from "@alepha/core";
12
+ import { $logger } from "@alepha/logger";
12
13
  import {
13
- apiLinksResponseSchema,
14
14
  type ServerHandler,
15
15
  ServerRouterProvider,
16
16
  ServerTimingProvider,
@@ -23,14 +23,12 @@ import {
23
23
  type PageDescriptorRenderOptions,
24
24
  } from "../descriptors/$page.ts";
25
25
  import { Redirection } from "../errors/Redirection.ts";
26
+ import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
26
27
  import {
27
- PageDescriptorProvider,
28
- type PageReactContext,
29
- type PageRequest,
30
28
  type PageRoute,
31
- type RouterState,
32
- } from "./PageDescriptorProvider.ts";
33
- import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
29
+ ReactPageProvider,
30
+ type ReactRouterState,
31
+ } from "./ReactPageProvider.ts";
34
32
 
35
33
  const envSchema = t.object({
36
34
  REACT_SERVER_DIST: t.string({ default: "public" }),
@@ -54,7 +52,7 @@ declare module "@alepha/core" {
54
52
  export class ReactServerProvider {
55
53
  protected readonly log = $logger();
56
54
  protected readonly alepha = $inject(Alepha);
57
- protected readonly pageDescriptorProvider = $inject(PageDescriptorProvider);
55
+ protected readonly pageApi = $inject(ReactPageProvider);
58
56
  protected readonly serverStaticProvider = $inject(ServerStaticProvider);
59
57
  protected readonly serverRouterProvider = $inject(ServerRouterProvider);
60
58
  protected readonly serverTimingProvider = $inject(ServerTimingProvider);
@@ -136,7 +134,7 @@ export class ReactServerProvider {
136
134
  }
137
135
 
138
136
  protected async registerPages(templateLoader: TemplateLoader) {
139
- for (const page of this.pageDescriptorProvider.getPages()) {
137
+ for (const page of this.pageApi.getPages()) {
140
138
  if (page.children?.length) {
141
139
  continue;
142
140
  }
@@ -197,38 +195,48 @@ export class ReactServerProvider {
197
195
  */
198
196
  protected createRenderFunction(name: string, withIndex = false) {
199
197
  return async (options: PageDescriptorRenderOptions = {}) => {
200
- const page = this.pageDescriptorProvider.page(name);
201
- const url = new URL(this.pageDescriptorProvider.url(name, options));
202
- const context: PageRequest = {
198
+ const page = this.pageApi.page(name);
199
+ const url = new URL(this.pageApi.url(name, options));
200
+
201
+ const entry: Partial<ReactRouterState> = {
203
202
  url,
204
203
  params: options.params ?? {},
205
204
  query: options.query ?? {},
206
- head: {},
207
205
  onError: () => null,
206
+ layers: [],
208
207
  };
209
208
 
209
+ const state = entry as ReactRouterState;
210
+
211
+ this.log.trace("Rendering", {
212
+ url,
213
+ });
214
+
210
215
  await this.alepha.emit("react:server:render:begin", {
211
- context,
216
+ state,
212
217
  });
213
218
 
214
- const state = await this.pageDescriptorProvider.createLayers(
219
+ const { redirect } = await this.pageApi.createLayers(
215
220
  page,
216
- context,
221
+ state as ReactRouterState,
217
222
  );
218
223
 
224
+ if (redirect) {
225
+ throw new AlephaError("Redirection is not supported in this context");
226
+ }
227
+
219
228
  if (!withIndex && !options.html) {
229
+ this.alepha.state("react.router.state", state);
230
+
220
231
  return {
221
- context,
222
- html: renderToString(
223
- this.pageDescriptorProvider.root(state, context),
224
- ),
232
+ state,
233
+ html: renderToString(this.pageApi.root(state)),
225
234
  };
226
235
  }
227
236
 
228
237
  const html = this.renderToHtml(
229
238
  this.template ?? "",
230
239
  state,
231
- context,
232
240
  options.hydration,
233
241
  );
234
242
 
@@ -237,7 +245,6 @@ export class ReactServerProvider {
237
245
  }
238
246
 
239
247
  const result = {
240
- context,
241
248
  state,
242
249
  html,
243
250
  };
@@ -249,7 +256,7 @@ export class ReactServerProvider {
249
256
  }
250
257
 
251
258
  protected createHandler(
252
- page: PageRoute,
259
+ route: PageRoute,
253
260
  templateLoader: TemplateLoader,
254
261
  ): ServerHandler {
255
262
  return async (serverRequest) => {
@@ -260,36 +267,32 @@ export class ReactServerProvider {
260
267
  }
261
268
 
262
269
  this.log.trace("Rendering page", {
263
- name: page.name,
270
+ name: route.name,
264
271
  });
265
272
 
266
- const context: PageRequest = {
273
+ const entry: Partial<ReactRouterState> = {
267
274
  url,
268
275
  params,
269
276
  query,
270
- // plugins
271
- head: {},
272
277
  onError: () => null,
278
+ layers: [],
273
279
  };
274
280
 
275
- if (this.alepha.has(ServerLinksProvider)) {
276
- const srv = this.alepha.inject(ServerLinksProvider);
277
- const schema = apiLinksResponseSchema as any;
281
+ const state = entry as ReactRouterState;
278
282
 
279
- context.links = this.alepha.parse(
280
- schema,
281
- await srv.getLinks({
282
- user: serverRequest.user,
283
+ if (this.alepha.has(ServerLinksProvider)) {
284
+ this.alepha.state(
285
+ "api",
286
+ await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
287
+ user: (serverRequest as any).user, // TODO: fix type
283
288
  authorization: serverRequest.headers.authorization,
284
289
  }),
285
- ) as any;
286
-
287
- this.alepha.context.set("links", context.links);
290
+ );
288
291
  }
289
292
 
290
- let target: PageRoute | undefined = page; // TODO: move to PageDescriptorProvider
293
+ let target: PageRoute | undefined = route; // TODO: move to PageDescriptorProvider
291
294
  while (target) {
292
- if (page.can && !page.can()) {
295
+ if (route.can && !route.can()) {
293
296
  // if the page is not accessible, return 403
294
297
  reply.status = 403;
295
298
  reply.headers["content-type"] = "text/plain";
@@ -309,27 +312,19 @@ export class ReactServerProvider {
309
312
  // return;
310
313
  // }
311
314
 
312
- await this.alepha.emit("react:transition:begin", {
313
- request: serverRequest,
314
- context,
315
- });
316
-
317
315
  await this.alepha.emit("react:server:render:begin", {
318
316
  request: serverRequest,
319
- context,
317
+ state,
320
318
  });
321
319
 
322
320
  this.serverTimingProvider.beginTiming("createLayers");
323
321
 
324
- const state = await this.pageDescriptorProvider.createLayers(
325
- page,
326
- context,
327
- );
322
+ const { redirect } = await this.pageApi.createLayers(route, state);
328
323
 
329
324
  this.serverTimingProvider.endTiming("createLayers");
330
325
 
331
- if (state.redirect) {
332
- return reply.redirect(state.redirect);
326
+ if (redirect) {
327
+ return reply.redirect(redirect);
333
328
  }
334
329
 
335
330
  reply.headers["content-type"] = "text/html";
@@ -341,34 +336,28 @@ export class ReactServerProvider {
341
336
  reply.headers.pragma = "no-cache";
342
337
  reply.headers.expires = "0";
343
338
 
344
- // don't cache user links
345
- if (page.cache && serverRequest.user) {
346
- delete context.links;
347
- }
348
-
349
- const html = this.renderToHtml(template, state, context);
339
+ const html = this.renderToHtml(template, state);
350
340
  if (html instanceof Redirection) {
351
341
  reply.redirect(
352
- typeof html.page === "string"
353
- ? html.page
354
- : this.pageDescriptorProvider.href(html.page),
342
+ typeof html.redirect === "string"
343
+ ? html.redirect
344
+ : this.pageApi.href(html.redirect),
355
345
  );
356
346
  return;
357
347
  }
358
348
 
359
349
  const event = {
360
350
  request: serverRequest,
361
- context,
362
351
  state,
363
352
  html,
364
353
  };
365
354
 
366
355
  await this.alepha.emit("react:server:render:end", event);
367
356
 
368
- page.onServerResponse?.(serverRequest);
357
+ route.onServerResponse?.(serverRequest);
369
358
 
370
359
  this.log.trace("Page rendered", {
371
- name: page.name,
360
+ name: route.name,
372
361
  });
373
362
 
374
363
  return event.html;
@@ -377,28 +366,32 @@ export class ReactServerProvider {
377
366
 
378
367
  public renderToHtml(
379
368
  template: string,
380
- state: RouterState,
381
- context: PageReactContext,
369
+ state: ReactRouterState,
382
370
  hydration = true,
383
371
  ): string | Redirection {
384
- const element = this.pageDescriptorProvider.root(state, context);
372
+ const element = this.pageApi.root(state);
385
373
 
386
- this.serverTimingProvider.beginTiming("renderToString");
374
+ // attach react router state to the http request context
375
+ this.alepha.state("react.router.state", state);
387
376
 
377
+ this.serverTimingProvider.beginTiming("renderToString");
388
378
  let app = "";
389
379
  try {
390
380
  app = renderToString(element);
391
381
  } catch (error) {
392
- this.log.error("Error during SSR", error);
393
- const element = context.onError(error as Error, context);
382
+ this.log.error(
383
+ "renderToString has failed, fallback to error handler",
384
+ error,
385
+ );
386
+ const element = state.onError(error as Error, state);
394
387
  if (element instanceof Redirection) {
395
388
  // if the error is a redirection, return the redirection URL
396
389
  return element;
397
390
  }
398
391
 
399
392
  app = renderToString(element);
393
+ this.log.debug("Error handled successfully with fallback");
400
394
  }
401
-
402
395
  this.serverTimingProvider.endTiming("renderToString");
403
396
 
404
397
  const response = {
@@ -406,11 +399,13 @@ export class ReactServerProvider {
406
399
  };
407
400
 
408
401
  if (hydration) {
409
- const { request, context, ...rest } =
410
- this.alepha.context.als?.getStore() ?? {};
402
+ const { request, context, ...store } =
403
+ this.alepha.context.als?.getStore() ?? {}; /// TODO: als must be protected, find a way to iterate on alepha.state
411
404
 
412
405
  const hydrationData: ReactHydrationState = {
413
- ...rest,
406
+ ...store,
407
+ // map react.router.state to the hydration state
408
+ "react.router.state": undefined,
414
409
  layers: state.layers.map((it) => ({
415
410
  ...it,
416
411
  error: it.error