bazis 0.98.10 → 0.98.11
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/core/http/Middleware/errorHandler.ts +19 -3
- package/core/http-client/index.ts +11 -7
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/library/http-client/HttpClient.ts +24 -1
- package/library/http-client/HttpClientFactory.ts +27 -17
- package/library/http-client/index.ts +1 -1
- package/library/http-client/serialize.ts +14 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { REQUEST_ID_STATE_KEY, type Logger } from "../../kernel";
|
|
2
2
|
import { redactSensitive, redactSensitiveText } from "../../../library/redaction";
|
|
3
|
+
import { HttpClientError, HttpErrorCode } from "../../../library/http-client/errors";
|
|
3
4
|
import type { HttpContext } from "../HttpContext/HttpContext";
|
|
4
5
|
import { HttpError } from "../Errors/HttpError";
|
|
5
6
|
import type { HttpMiddleware } from "./types";
|
|
@@ -124,15 +125,30 @@ export function errorHandler(options: ErrorHandlerOptions = {}): HttpMiddleware
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
safelyReportUnexpected(options, log, ctx, error);
|
|
128
|
+
const [status, title] = unexpectedStatus(error);
|
|
127
129
|
ctx.response = options.exposeDetails
|
|
128
|
-
? safeJsonResponse(
|
|
129
|
-
error:
|
|
130
|
+
? safeJsonResponse(status, {
|
|
131
|
+
error: title,
|
|
130
132
|
// Development only, but redacted like the log: a secret in an error
|
|
131
133
|
// message must not reach a browser or a shared screenshot.
|
|
132
134
|
message: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|
|
133
135
|
stack: error instanceof Error && error.stack !== undefined ? redactSensitiveText(error.stack) : undefined,
|
|
134
136
|
})
|
|
135
|
-
: safeJsonResponse(
|
|
137
|
+
: safeJsonResponse(status, { error: title });
|
|
136
138
|
}
|
|
137
139
|
};
|
|
138
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* An unhandled outbound HTTP failure is the upstream's fault, not ours:
|
|
144
|
+
* 504 when the call timed out, 502 otherwise. A client error without a code
|
|
145
|
+
* is a misconfiguration of our request (for example an invalid maxRedirects)
|
|
146
|
+
* and stays 500, like everything else. Upstream details never reach the
|
|
147
|
+
* client; the log keeps the full error.
|
|
148
|
+
*/
|
|
149
|
+
function unexpectedStatus(error: unknown): readonly [number, string] {
|
|
150
|
+
if (error instanceof HttpClientError && error.code !== undefined) {
|
|
151
|
+
return error.code === HttpErrorCode.Timeout ? [504, "Gateway Timeout"] : [502, "Bad Gateway"];
|
|
152
|
+
}
|
|
153
|
+
return [500, "Internal Server Error"];
|
|
154
|
+
}
|
|
@@ -16,8 +16,8 @@ import { DI, Module, createToken, type BazisModule, type ProviderDefinition } fr
|
|
|
16
16
|
import { getOutboundCorrelationHeaders } from "../kernel";
|
|
17
17
|
import {
|
|
18
18
|
HttpClient,
|
|
19
|
+
HttpClientFactory,
|
|
19
20
|
HttpClientFactoryBuilder,
|
|
20
|
-
type HttpClientFactory,
|
|
21
21
|
type HeaderBag,
|
|
22
22
|
type RequestConfig,
|
|
23
23
|
} from "../../library/http-client";
|
|
@@ -44,9 +44,10 @@ export interface HttpClientModuleConfig {
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Registers the outbound HTTP client in DI with correlation propagation wired
|
|
47
|
-
* to the kernel request context:
|
|
48
|
-
* - {@link
|
|
49
|
-
* - {@link
|
|
47
|
+
* to the kernel request context. A service takes it by type:
|
|
48
|
+
* - `HttpClient` (or the {@link HTTP_CLIENT} token) — the default client;
|
|
49
|
+
* - `HttpClientFactory` (or {@link HTTP_CLIENT_FACTORY}) — named clients
|
|
50
|
+
* (fail-fast on unknown names).
|
|
50
51
|
*/
|
|
51
52
|
export function httpClientModule(config: HttpClientModuleConfig = {}): BazisModule {
|
|
52
53
|
const correlationHeaders = config.correlationHeaders ?? getOutboundCorrelationHeaders;
|
|
@@ -83,11 +84,14 @@ export function httpClientModule(config: HttpClientModuleConfig = {}): BazisModu
|
|
|
83
84
|
}
|
|
84
85
|
const factory = builder.build();
|
|
85
86
|
|
|
87
|
+
const client = factory.createClient();
|
|
88
|
+
// One factory and one default client, under the class (injection by type)
|
|
89
|
+
// and under the original tokens.
|
|
86
90
|
const providers: ProviderDefinition[] = [
|
|
91
|
+
DI.singleton(DI.valueProvider(HttpClientFactory, factory)),
|
|
87
92
|
DI.singleton(DI.valueProvider(HTTP_CLIENT_FACTORY, factory)),
|
|
88
|
-
DI.singleton(
|
|
89
|
-
|
|
90
|
-
),
|
|
93
|
+
DI.singleton(DI.valueProvider(HttpClient, client)),
|
|
94
|
+
DI.singleton(DI.valueProvider(HTTP_CLIENT, client)),
|
|
91
95
|
];
|
|
92
96
|
|
|
93
97
|
@Module({ providers })
|
|
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
|
|
|
232
232
|
if (method === "account/updated") this.modelCache = undefined;
|
|
233
233
|
}, () => { this.clearLogin(); this.modelCache = undefined; });
|
|
234
234
|
const initialized = await client.request("initialize", {
|
|
235
|
-
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.
|
|
235
|
+
clientInfo: { name: "bazis", title: "Bazis", version: "0.98.11" }, capabilities: { experimentalApi: true },
|
|
236
236
|
}, 15_000, signal);
|
|
237
237
|
if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
|
|
238
238
|
client.notify("initialized");
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
appendQuery,
|
|
7
7
|
basicAuthHeader,
|
|
8
8
|
isBodyInit,
|
|
9
|
+
isAbsoluteUrl,
|
|
9
10
|
mergeHeaderBags,
|
|
11
|
+
pageUrl,
|
|
10
12
|
resolveUrl,
|
|
11
13
|
shouldPropagateCorrelation,
|
|
12
14
|
} from "./serialize";
|
|
@@ -188,7 +190,8 @@ export class HttpClient {
|
|
|
188
190
|
return { ...merged, method: String(override.method ?? base.method ?? "GET").toUpperCase() };
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
private async dispatch<T>(
|
|
193
|
+
private async dispatch<T>(requested: ResolvedRequestConfig): Promise<HttpResponse<T>> {
|
|
194
|
+
const config = withAbsoluteBaseUrl(requested);
|
|
192
195
|
if (config.inspectableRedirects !== undefined && typeof config.inspectableRedirects !== "boolean") {
|
|
193
196
|
throw new HttpClientError("inspectableRedirects must be a boolean", { config });
|
|
194
197
|
}
|
|
@@ -650,6 +653,26 @@ function isReplayableBody(body: RequestInit["body"]): boolean {
|
|
|
650
653
|
|| (typeof ReadableStream !== "undefined" && body instanceof ReadableStream));
|
|
651
654
|
}
|
|
652
655
|
|
|
656
|
+
/**
|
|
657
|
+
* A relative `baseUrl` (`"/api"`) is resolved against the page address once,
|
|
658
|
+
* so URL building and every origin check below see an absolute base. Outside
|
|
659
|
+
* a page there is nothing to resolve it against.
|
|
660
|
+
*/
|
|
661
|
+
function withAbsoluteBaseUrl(config: ResolvedRequestConfig): ResolvedRequestConfig {
|
|
662
|
+
const { baseUrl } = config;
|
|
663
|
+
if (!baseUrl || isAbsoluteUrl(baseUrl)) {
|
|
664
|
+
return config;
|
|
665
|
+
}
|
|
666
|
+
const page = pageUrl();
|
|
667
|
+
if (page === undefined) {
|
|
668
|
+
throw new HttpClientError(
|
|
669
|
+
`baseUrl "${baseUrl}" is relative: outside a browser page it must be absolute, for example "http://localhost:3000${baseUrl.startsWith("/") ? "" : "/"}${baseUrl}"`,
|
|
670
|
+
{ config },
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return { ...config, baseUrl: new URL(baseUrl, page).toString() };
|
|
674
|
+
}
|
|
675
|
+
|
|
653
676
|
function sameOrigin(left: string, right: string): boolean {
|
|
654
677
|
try {
|
|
655
678
|
return new URL(left).origin === new URL(right).origin;
|
|
@@ -2,9 +2,32 @@ import { HttpClient } from "./HttpClient";
|
|
|
2
2
|
import { HttpClientConfigError } from "./errors";
|
|
3
3
|
import type { RequestConfig } from "./types";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Resolves pre-configured named clients (à la .NET `IHttpClientFactory`).
|
|
7
|
+
* An abstract class, so a service can take it by type in its constructor.
|
|
8
|
+
*/
|
|
9
|
+
export abstract class HttpClientFactory {
|
|
10
|
+
public abstract createClient(name?: string): HttpClient;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class ConfiguredHttpClientFactory extends HttpClientFactory {
|
|
14
|
+
public constructor(
|
|
15
|
+
private readonly defaults: RequestConfig,
|
|
16
|
+
private readonly named: ReadonlyMap<string, RequestConfig>,
|
|
17
|
+
) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public createClient(name?: string): HttpClient {
|
|
22
|
+
if (name === undefined) {
|
|
23
|
+
return new HttpClient(this.defaults);
|
|
24
|
+
}
|
|
25
|
+
const config = this.named.get(name);
|
|
26
|
+
if (config === undefined) {
|
|
27
|
+
throw new HttpClientConfigError(name, [...this.named.keys()]);
|
|
28
|
+
}
|
|
29
|
+
return new HttpClient(this.defaults).create(config);
|
|
30
|
+
}
|
|
8
31
|
}
|
|
9
32
|
|
|
10
33
|
/**
|
|
@@ -27,19 +50,6 @@ export class HttpClientFactoryBuilder {
|
|
|
27
50
|
}
|
|
28
51
|
|
|
29
52
|
public build(): HttpClientFactory {
|
|
30
|
-
|
|
31
|
-
const defaults = this.defaults;
|
|
32
|
-
return {
|
|
33
|
-
createClient(name?: string): HttpClient {
|
|
34
|
-
if (name === undefined) {
|
|
35
|
-
return new HttpClient(defaults);
|
|
36
|
-
}
|
|
37
|
-
const config = named.get(name);
|
|
38
|
-
if (config === undefined) {
|
|
39
|
-
throw new HttpClientConfigError(name, [...named.keys()]);
|
|
40
|
-
}
|
|
41
|
-
return new HttpClient(defaults).create(config);
|
|
42
|
-
},
|
|
43
|
-
};
|
|
53
|
+
return new ConfiguredHttpClientFactory(this.defaults, new Map(this.named));
|
|
44
54
|
}
|
|
45
55
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export { HttpClient } from "./HttpClient";
|
|
14
14
|
export { InspectableRedirectProtocol } from "./redirectProtocol";
|
|
15
|
-
export { HttpClientFactoryBuilder,
|
|
15
|
+
export { HttpClientFactoryBuilder, HttpClientFactory } from "./HttpClientFactory";
|
|
16
16
|
export { HttpClientError, HttpClientConfigError, HttpErrorCode } from "./errors";
|
|
17
17
|
export { InterceptorManager, type FulfilledFn, type RejectedFn } from "./interceptors";
|
|
18
18
|
export {
|
|
@@ -8,6 +8,20 @@ const ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
|
8
8
|
* prefix), `baseUrl: "https://api/v1"` + `"/users"` → `…/v1/users`.
|
|
9
9
|
* Absolute URLs and missing baseUrl pass through.
|
|
10
10
|
*/
|
|
11
|
+
export function isAbsoluteUrl(url: string): boolean {
|
|
12
|
+
return ABSOLUTE_URL.test(url);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The address of the current page in a browser (or worker), else undefined.
|
|
17
|
+
* A relative `baseUrl` such as `"/api"` is resolved against it, like axios's
|
|
18
|
+
* `baseURL` in a browser app.
|
|
19
|
+
*/
|
|
20
|
+
export function pageUrl(): string | undefined {
|
|
21
|
+
const location = (globalThis as { location?: { href?: unknown } }).location;
|
|
22
|
+
return typeof location?.href === "string" ? location.href : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export function resolveUrl(baseUrl: string | undefined, url: string): string {
|
|
12
26
|
if (!baseUrl || ABSOLUTE_URL.test(url)) {
|
|
13
27
|
return url;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.98.
|
|
3
|
+
"version": "0.98.11",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|