@lukoweb/apitogo 0.2.31 → 0.2.34
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/dist/cli/cli.js +436 -153
- package/dist/declarations/app/entry.server.d.ts +2 -2
- package/dist/declarations/app/main.d.ts +3 -1
- package/dist/declarations/cli/platform/client/platform-api.d.ts +0 -4
- package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
- package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
- package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
- package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
- package/dist/declarations/config/effective-redirects.d.ts +6 -0
- package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
- package/dist/declarations/config/resolve-modules.d.ts +1 -4
- package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
- package/docs/cli/commands.md +19 -0
- package/docs/configuration/gateway.md +52 -0
- package/docs/guides/customer-account.md +11 -1
- package/docs/mcp/tools.md +101 -102
- package/package.json +2 -1
- package/src/app/demo.tsx +78 -77
- package/src/app/entry.client.tsx +115 -107
- package/src/app/entry.server.tsx +164 -160
- package/src/app/main.tsx +175 -167
- package/src/app/standalone.tsx +61 -60
- package/src/config/effective-redirects.ts +31 -0
- package/src/config/header-navigation-guard.ts +13 -0
- package/src/config/resolve-modules.ts +331 -354
- package/src/config/v2/home-redirect.ts +104 -98
- package/src/config/v2/index.ts +127 -126
- package/src/config/v2/ledger.ts +242 -235
- package/src/config/v2/resolve.ts +12 -4
- package/src/config/v2/rules.ts +716 -714
- package/src/config/v2/services.ts +799 -663
- package/src/config/v2/workspace.ts +17 -1
- package/src/config/validators/HeaderNavigationSchema.ts +42 -44
- package/src/lib/assets/powered-by-icon.png +0 -0
- package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
- package/src/lib/authentication/providers/dev-portal.tsx +476 -448
- package/src/lib/components/HeaderNavigation.tsx +176 -176
- package/src/lib/components/MobileTopNavigation.tsx +283 -283
- package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
- package/src/vite/plugin-offroot-resolve.ts +68 -67
- package/src/vite/prerender/prerender.ts +242 -242
package/src/app/entry.client.tsx
CHANGED
|
@@ -1,107 +1,115 @@
|
|
|
1
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
2
|
-
import {
|
|
3
|
-
createBrowserRouter,
|
|
4
|
-
matchRoutes,
|
|
5
|
-
type RouteObject,
|
|
6
|
-
} from "react-router";
|
|
7
|
-
import config from "virtual:zudoku-config";
|
|
8
|
-
import "vite/modulepreload-polyfill";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
1
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
2
|
+
import {
|
|
3
|
+
createBrowserRouter,
|
|
4
|
+
matchRoutes,
|
|
5
|
+
type RouteObject,
|
|
6
|
+
} from "react-router";
|
|
7
|
+
import config from "virtual:zudoku-config";
|
|
8
|
+
import "vite/modulepreload-polyfill";
|
|
9
|
+
import type { ResolvedModulesConfig } from "../config/validators/ModulesSchema.js";
|
|
10
|
+
import { Bootstrap } from "../lib/components/Bootstrap.js";
|
|
11
|
+
import { getRoutesByConfig } from "./main.js";
|
|
12
|
+
|
|
13
|
+
// The build resolved the modules and put them in the config; parsing them again would put zod in this bundle.
|
|
14
|
+
const routes = getRoutesByConfig(
|
|
15
|
+
config,
|
|
16
|
+
(c) => (c as { __resolvedModules: ResolvedModulesConfig }).__resolvedModules,
|
|
17
|
+
);
|
|
18
|
+
// biome-ignore lint/style/noNonNullAssertion: We know the root element exists
|
|
19
|
+
const root = document.getElementById("root")!;
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
ZUDOKU_VERSION: string;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
window.ZUDOKU_VERSION = process.env.ZUDOKU_VERSION ?? "unknown";
|
|
28
|
+
|
|
29
|
+
if (process.env.SENTRY_DSN) {
|
|
30
|
+
void import("./sentry.js").then((mod) => {
|
|
31
|
+
mod.initSentry({ dsn: process.env.SENTRY_DSN as string });
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (root.childElementCount > 0) {
|
|
36
|
+
void hydrate(routes);
|
|
37
|
+
} else {
|
|
38
|
+
void render(routes);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (import.meta.env.IS_ZUPLO) {
|
|
42
|
+
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
43
|
+
console.log(
|
|
44
|
+
`%cPowered by Zuplo v${window.ZUDOKU_VERSION}`,
|
|
45
|
+
[
|
|
46
|
+
"color: #Df0097",
|
|
47
|
+
"line-height: 50px",
|
|
48
|
+
"font-weight: bolder",
|
|
49
|
+
"font-size: 15px",
|
|
50
|
+
"text-align: center",
|
|
51
|
+
"letter-spacing: 5px",
|
|
52
|
+
].join(" ;"),
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
56
|
+
console.log(
|
|
57
|
+
`%cAPITOGO v${window.ZUDOKU_VERSION}`,
|
|
58
|
+
[
|
|
59
|
+
"color: #0d9488",
|
|
60
|
+
"line-height: 50px",
|
|
61
|
+
"font-weight: bolder",
|
|
62
|
+
"font-size: 15px",
|
|
63
|
+
"text-align: center",
|
|
64
|
+
"letter-spacing: 5px",
|
|
65
|
+
].join(" ;"),
|
|
66
|
+
);
|
|
67
|
+
// biome-ignore lint/suspicious/noConsole: Logging allowed here
|
|
68
|
+
console.log("» Learn more at https://apitogo.com");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function hydrateLazyRoutes(routes: RouteObject[]) {
|
|
72
|
+
const path = window.location.pathname;
|
|
73
|
+
const lazyMatches = matchRoutes(routes, path, config.basePath)?.filter(
|
|
74
|
+
(m) => m.route.lazy,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (lazyMatches?.length) {
|
|
78
|
+
await Promise.all(
|
|
79
|
+
lazyMatches.map(async (m) => {
|
|
80
|
+
if (typeof m.route.lazy !== "function") return;
|
|
81
|
+
|
|
82
|
+
const routeModule = await m.route.lazy();
|
|
83
|
+
Object.assign(m.route, { ...routeModule, lazy: undefined });
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function render(routes: RouteObject[]) {
|
|
90
|
+
const router = createBrowserRouter(routes, {
|
|
91
|
+
basename: config.basePath,
|
|
92
|
+
});
|
|
93
|
+
createRoot(root).render(<Bootstrap router={router} />);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function hydrate(routes: RouteObject[]) {
|
|
97
|
+
// Only the route chunks are awaited. The highlighter keeps loading in the background: whatever needs it reads
|
|
98
|
+
// it with use() and waits on its own, with the prerendered HTML staying on screen, so a page without code
|
|
99
|
+
// (pricing, account, checkout) is not held back by ~260 KB of grammars it never uses.
|
|
100
|
+
await hydrateLazyRoutes(routes);
|
|
101
|
+
|
|
102
|
+
const router = createBrowserRouter(routes, {
|
|
103
|
+
basename: config.basePath,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
hydrateRoot(root, <Bootstrap hydrate router={router} />);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// This is a workaround to avoid version skewing
|
|
110
|
+
// See https://vite.dev/guide/build.html#load-error-handling
|
|
111
|
+
// TODO: Implement a more advanced solution if there are CDN urls or e.g. Vercel Skew Protection
|
|
112
|
+
window.addEventListener("vite:preloadError", (e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
window.location.reload();
|
|
115
|
+
});
|
package/src/app/entry.server.tsx
CHANGED
|
@@ -1,160 +1,164 @@
|
|
|
1
|
-
import { dehydrate, QueryClient } from "@tanstack/react-query";
|
|
2
|
-
import type { HelmetData } from "@zudoku/react-helmet-async";
|
|
3
|
-
import logger from "loglevel";
|
|
4
|
-
import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
|
|
5
|
-
import {
|
|
6
|
-
createStaticHandler,
|
|
7
|
-
createStaticRouter,
|
|
8
|
-
isRouteErrorResponse,
|
|
9
|
-
type RouteObject,
|
|
10
|
-
} from "react-router";
|
|
11
|
-
import "vite/modulepreload-polyfill";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
helmetContext.helmet?.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
controller.enqueue(encoder.encode(htmlEnd.slice(idx)));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
import { dehydrate, QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { HelmetData } from "@zudoku/react-helmet-async";
|
|
3
|
+
import logger from "loglevel";
|
|
4
|
+
import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
|
|
5
|
+
import {
|
|
6
|
+
createStaticHandler,
|
|
7
|
+
createStaticRouter,
|
|
8
|
+
isRouteErrorResponse,
|
|
9
|
+
type RouteObject,
|
|
10
|
+
} from "react-router";
|
|
11
|
+
import "vite/modulepreload-polyfill";
|
|
12
|
+
import type { ApitogoConfig } from "../config/config.js";
|
|
13
|
+
import { resolveModulesConfig } from "../config/resolve-modules.js";
|
|
14
|
+
import { BootstrapStatic } from "../lib/components/Bootstrap.js";
|
|
15
|
+
import { NO_DEHYDRATE } from "../lib/components/cache.js";
|
|
16
|
+
import { ServerError } from "../lib/errors/ServerError.js";
|
|
17
|
+
import { highlighterPromise } from "../lib/shiki.js";
|
|
18
|
+
import { getRoutesByConfig as routesByConfig } from "./main.js";
|
|
19
|
+
|
|
20
|
+
export const getRoutesByConfig = (config: ApitogoConfig) =>
|
|
21
|
+
routesByConfig(config, resolveModulesConfig);
|
|
22
|
+
|
|
23
|
+
// Statically importing shiki.ts here ensures it's in the SSR bundle.
|
|
24
|
+
// main.tsx dynamically imports it instead to enable lazy loading on the client.
|
|
25
|
+
await import("virtual:zudoku-shiki-register").then(
|
|
26
|
+
async ({ registerShiki }) => {
|
|
27
|
+
const highlighter = await highlighterPromise;
|
|
28
|
+
await registerShiki(highlighter);
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
export const handleRequest = async ({
|
|
32
|
+
template,
|
|
33
|
+
request,
|
|
34
|
+
routes,
|
|
35
|
+
basePath,
|
|
36
|
+
bypassProtection,
|
|
37
|
+
}: {
|
|
38
|
+
template: string;
|
|
39
|
+
request: Request;
|
|
40
|
+
routes: RouteObject[];
|
|
41
|
+
basePath?: string;
|
|
42
|
+
bypassProtection?: boolean;
|
|
43
|
+
}): Promise<Response> => {
|
|
44
|
+
const { query, dataRoutes } = createStaticHandler(routes, {
|
|
45
|
+
basename: basePath,
|
|
46
|
+
});
|
|
47
|
+
const queryClient = new QueryClient();
|
|
48
|
+
|
|
49
|
+
const context = await query(request);
|
|
50
|
+
|
|
51
|
+
if (context instanceof Response) {
|
|
52
|
+
if ([301, 302, 303, 307, 308].includes(context.status)) {
|
|
53
|
+
return new Response(null, {
|
|
54
|
+
status: context.status,
|
|
55
|
+
headers: { Location: context.headers.get("Location") ?? "" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
throw context;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let status = 200;
|
|
62
|
+
if (context.errors) {
|
|
63
|
+
const firstError = Object.values(context.errors).find(isRouteErrorResponse);
|
|
64
|
+
if (firstError?.status) {
|
|
65
|
+
status = firstError.status;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const router = createStaticRouter(dataRoutes, context);
|
|
70
|
+
const helmetContext = {} as HelmetData["context"];
|
|
71
|
+
const renderContext = {
|
|
72
|
+
status: 200,
|
|
73
|
+
bypassProtection: bypassProtection ?? false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const App = (
|
|
77
|
+
<BootstrapStatic
|
|
78
|
+
router={router}
|
|
79
|
+
context={context}
|
|
80
|
+
queryClient={queryClient}
|
|
81
|
+
helmetContext={helmetContext}
|
|
82
|
+
bypassProtection={bypassProtection}
|
|
83
|
+
renderContext={renderContext}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const reactStream = await renderToReadableStream(App, {
|
|
89
|
+
onError(error) {
|
|
90
|
+
status = 500;
|
|
91
|
+
if (import.meta.env.PROD) {
|
|
92
|
+
logger.error("SSR Error:", error);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await reactStream.allReady;
|
|
98
|
+
|
|
99
|
+
const [htmlStart, htmlEnd] = template.split("<!--app-html-->");
|
|
100
|
+
if (!htmlStart) {
|
|
101
|
+
throw new Error("No <!--app-html--> found in template");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const encoder = new TextEncoder();
|
|
105
|
+
const reader = reactStream.getReader();
|
|
106
|
+
|
|
107
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
108
|
+
async start(controller) {
|
|
109
|
+
const helmetHtml = [
|
|
110
|
+
helmetContext.helmet?.title?.toString() ?? "",
|
|
111
|
+
helmetContext.helmet?.meta?.toString() ?? "",
|
|
112
|
+
helmetContext.helmet?.link?.toString() ?? "",
|
|
113
|
+
helmetContext.helmet?.style?.toString() ?? "",
|
|
114
|
+
helmetContext.helmet?.script?.toString() ?? "",
|
|
115
|
+
].join("\n");
|
|
116
|
+
|
|
117
|
+
controller.enqueue(
|
|
118
|
+
encoder.encode(htmlStart.replace("<!--app-helmet-->", helmetHtml)),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
while (true) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done) break;
|
|
124
|
+
controller.enqueue(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (htmlEnd) {
|
|
128
|
+
const dehydrated = dehydrate(queryClient, {
|
|
129
|
+
shouldDehydrateQuery: (q) => !q.queryKey.includes(NO_DEHYDRATE),
|
|
130
|
+
});
|
|
131
|
+
const serialized = JSON.stringify(dehydrated)
|
|
132
|
+
.replace(/</g, "\\u003c")
|
|
133
|
+
.replace(/>/g, "\\u003e");
|
|
134
|
+
|
|
135
|
+
const closingTag = "</body>";
|
|
136
|
+
const idx = htmlEnd.lastIndexOf(closingTag);
|
|
137
|
+
|
|
138
|
+
if (idx === -1) {
|
|
139
|
+
controller.enqueue(encoder.encode(htmlEnd));
|
|
140
|
+
} else {
|
|
141
|
+
controller.enqueue(encoder.encode(htmlEnd.slice(0, idx)));
|
|
142
|
+
controller.enqueue(
|
|
143
|
+
encoder.encode(`<script>window.DATA=${serialized}</script>`),
|
|
144
|
+
);
|
|
145
|
+
controller.enqueue(encoder.encode(htmlEnd.slice(idx)));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
controller.close();
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return new Response(stream, {
|
|
154
|
+
status: renderContext.status !== 200 ? renderContext.status : status,
|
|
155
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const html = renderToStaticMarkup(<ServerError error={error} />);
|
|
159
|
+
return new Response(html, {
|
|
160
|
+
status: 500,
|
|
161
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|