@astrale-os/kernel-server 0.5.0-beta.65 → 0.5.0-beta.67

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.
@@ -1,3 +1,11 @@
1
1
  import * as errors from '@astrale-os/kernel-protocol/errors';
2
2
  /** Convert only errors owned by an admitted Server or Protocol boundary into safe public data. */
3
3
  export declare function classifyError(input: unknown): errors.ErrorPayload;
4
+ /**
5
+ * Classify the failure of one request. Its own deadline or cancellation names the failure, except an
6
+ * outcome already unknown: ending the request proves nothing about an effect.
7
+ */
8
+ export declare function classifyRequestError(input: unknown, request: {
9
+ readonly timedOut: boolean;
10
+ readonly signal: AbortSignal;
11
+ }): errors.ErrorPayload;
@@ -17,6 +17,21 @@ export function classifyError(input) {
17
17
  }
18
18
  return internal(input);
19
19
  }
20
+ /**
21
+ * Classify the failure of one request. Its own deadline or cancellation names the failure, except an
22
+ * outcome already unknown: ending the request proves nothing about an effect.
23
+ */
24
+ export function classifyRequestError(input, request) {
25
+ if (input instanceof errors.ProtocolError &&
26
+ input.payload.code === errors.CODES.OUTCOME_UNKNOWN) {
27
+ return input.payload;
28
+ }
29
+ if (request.timedOut)
30
+ return publicError('DEADLINE_EXCEEDED', 'Invocation deadline exceeded.');
31
+ if (request.signal.aborted)
32
+ return publicError('CANCELLED', 'Invocation was cancelled.');
33
+ return classifyError(input);
34
+ }
20
35
  function publicError(name, message) {
21
36
  return { code: errors.CODES[name], message };
22
37
  }
@@ -1,5 +1,5 @@
1
1
  export { ADMISSION_RETRY_AFTER_MS, admissionRefusal, retryAfterHeaders } from './admission.js';
2
2
  export { DeliveryUnavailableError, IssuerKeysUnavailableError } from './availability.js';
3
- export { classifyError } from './classify.js';
3
+ export { classifyError, classifyRequestError } from './classify.js';
4
4
  export { RequestBodyInvalidError, RequestBodyTooLargeError } from './request-body.js';
5
5
  export { ResponseBodyTooLargeError } from './response-body.js';
@@ -1,5 +1,5 @@
1
1
  export { ADMISSION_RETRY_AFTER_MS, admissionRefusal, retryAfterHeaders } from './admission.js';
2
2
  export { DeliveryUnavailableError, IssuerKeysUnavailableError } from './availability.js';
3
- export { classifyError } from './classify.js';
3
+ export { classifyError, classifyRequestError } from './classify.js';
4
4
  export { RequestBodyInvalidError, RequestBodyTooLargeError } from './request-body.js';
5
5
  export { ResponseBodyTooLargeError } from './response-body.js';
@@ -5,7 +5,7 @@ import { bundleUrl } from '@astrale-os/kernel-protocol/publication';
5
5
  import { mediaType as publicationMediaType, PATH as PUBLICATION_PATH, url as publicationUrl, } from '@astrale-os/kernel-protocol/publication/publication';
6
6
  import { reference as publicationReference } from '@astrale-os/kernel-protocol/publication/reference';
7
7
  import * as routes from '@astrale-os/kernel-protocol/routes';
8
- import { admissionRefusal, classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
8
+ import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, retryAfterHeaders, } from '../errors/index.js';
9
9
  import { invocationContext } from '../invocation/index.js';
10
10
  import { handleDelivery } from '../publication/delivery/index.js';
11
11
  import { readPreparedPublicationSnapshot, } from '../publication/index.js';
@@ -203,16 +203,7 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
203
203
  if (cause instanceof routes.RouteError) {
204
204
  return nativeError({ code: errors.CODES.INVALID_REQUEST, message: 'Native route request is invalid.' }, 400, corsFor);
205
205
  }
206
- if (operation.timedOut) {
207
- return nativeError({
208
- code: errors.CODES.DEADLINE_EXCEEDED,
209
- message: 'Invocation deadline exceeded.',
210
- }, 504, corsFor);
211
- }
212
- if (operation.signal.aborted) {
213
- return nativeError({ code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, 408, corsFor);
214
- }
215
- const payload = classifyError(cause);
206
+ const payload = classifyRequestError(cause, operation);
216
207
  return nativeError(payload, errors.httpStatusOf(payload), corsFor);
217
208
  }
218
209
  finally {
@@ -396,13 +387,13 @@ async function streamResponse(stream, operation, cors) {
396
387
  const cleanup = cancelSetup('native stream setup failed');
397
388
  if (!operation.signal.aborted)
398
389
  await cleanup;
399
- const payload = preStreamError(cause, operation);
390
+ const payload = classifyRequestError(cause, operation);
400
391
  return nativeError(payload, errors.httpStatusOf(payload), cors);
401
392
  }
402
393
  unregisterSetupCancellation();
403
394
  if (operation.signal.aborted) {
404
395
  void cancelSetup('native stream setup cancelled');
405
- const payload = preStreamError(operation.signal.reason, operation);
396
+ const payload = classifyRequestError(operation.signal.reason, operation);
406
397
  return response({ error: payload }, errors.httpStatusOf(payload), cors());
407
398
  }
408
399
  const encoder = new TextEncoder();
@@ -487,21 +478,6 @@ async function streamResponse(stream, operation, cors) {
487
478
  },
488
479
  });
489
480
  }
490
- function preStreamError(cause, operation) {
491
- if (operation.timedOut) {
492
- return {
493
- code: errors.CODES.DEADLINE_EXCEEDED,
494
- message: 'Invocation deadline exceeded before the native stream began.',
495
- };
496
- }
497
- if (operation.signal.aborted) {
498
- return {
499
- code: errors.CODES.CANCELLED,
500
- message: 'Invocation was cancelled before the native stream began.',
501
- };
502
- }
503
- return classifyError(cause);
504
- }
505
481
  function sameRoute(left, right, externalOrigin) {
506
482
  const a = new URL(left);
507
483
  const b = new URL(right);
@@ -1,7 +1,7 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as errors from '@astrale-os/kernel-protocol/errors';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import { admissionRefusal, classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
4
+ import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
5
5
  import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
6
6
  import { readBoundedBody } from '../body.js';
7
7
  import { loadCodecs } from '../codecs.js';
@@ -137,11 +137,7 @@ export function createHttpHandler(config) {
137
137
  active.extend(config.binaryDownloadTimeoutMs);
138
138
  }
139
139
  if (active.signal.aborted) {
140
- const timedOut = active.timedOut;
141
- return renderError(envelope.requestId, {
142
- code: timedOut ? errors.CODES.DEADLINE_EXCEEDED : errors.CODES.CANCELLED,
143
- message: timedOut ? 'Invocation deadline exceeded.' : 'Invocation was cancelled.',
144
- }, outgoing, { status: timedOut ? 504 : 408 });
140
+ return renderError(envelope.requestId, classifyRequestError(active.signal.reason, active), outgoing);
145
141
  }
146
142
  const publication = config.currentPublication?.(request);
147
143
  if (config.currentPublication !== undefined && publication === undefined) {
@@ -202,16 +198,7 @@ export function createHttpHandler(config) {
202
198
  return response;
203
199
  }
204
200
  catch (input) {
205
- if (active.timedOut) {
206
- return renderError(envelope.requestId, {
207
- code: errors.CODES.DEADLINE_EXCEEDED,
208
- message: 'Invocation deadline exceeded.',
209
- }, outgoing, { status: 504 });
210
- }
211
- if (active.signal.aborted) {
212
- return renderError(envelope.requestId, { code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, outgoing, { status: 408 });
213
- }
214
- return renderError(envelope.requestId, classifyError(input), outgoing);
201
+ return renderError(envelope.requestId, classifyRequestError(input, active), outgoing);
215
202
  }
216
203
  }
217
204
  catch (input) {
@@ -105,10 +105,15 @@ function streamingBinaryHttpResponse(binary, context) {
105
105
  mediaType: binary.mediaType,
106
106
  headers: binary.headers,
107
107
  }), context.cors, true);
108
- if (binary.length !== undefined)
108
+ if (binary.length !== undefined) {
109
109
  headers.set('content-length', String(binary.length));
110
- if (delivery !== undefined)
111
- headers.set('cache-control', 'private, no-store');
110
+ // Content-Length belongs to HTTP and an intermediary may rewrite it; the Client counts the
111
+ // delivered bytes against this protocol field instead.
112
+ headers.set(protocol.http.BINARY_LENGTH_FIELD, String(binary.length));
113
+ }
114
+ // Clients that predate the protocol length count against Content-Length, which an intermediary
115
+ // that re-encodes the stream would rewrite, so a compressible media type would fail delivery.
116
+ headers.set('cache-control', delivery === undefined ? 'no-store, no-transform' : 'private, no-store, no-transform');
112
117
  let terminal = false;
113
118
  let delivered = 0;
114
119
  let controller;
@@ -207,7 +212,9 @@ function binaryHttpHeaders(binary, cors, streaming) {
207
212
  protocol.http.REQUEST_ID_FIELD,
208
213
  protocol.http.INVOCATION_FIELD,
209
214
  protocol.http.BINARY_HEADERS_FIELD,
210
- ...(streaming ? [protocol.http.BINARY_DELIVERY_FIELD] : []),
215
+ ...(streaming
216
+ ? [protocol.http.BINARY_DELIVERY_FIELD, protocol.http.BINARY_LENGTH_FIELD]
217
+ : []),
211
218
  ...applicationHeaders,
212
219
  ]));
213
220
  for (const [name, value] of Object.entries(binary.headers ?? {})) {
@@ -1,6 +1,6 @@
1
1
  import * as errors from '@astrale-os/kernel-protocol/errors';
2
2
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
3
- import { admissionRefusal, classifyError } from '../../errors/index.js';
3
+ import { admissionRefusal, classifyError, classifyRequestError } from '../../errors/index.js';
4
4
  import { ResponseBodyTooLargeError } from '../../errors/index.js';
5
5
  import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
6
6
  import { byteLength } from '../body.js';
@@ -152,14 +152,7 @@ export function createWebSocketSession(config) {
152
152
  return;
153
153
  }
154
154
  if (socket.readyState === WS_OPEN) {
155
- const error = operation.timedOut
156
- ? {
157
- code: errors.CODES.DEADLINE_EXCEEDED,
158
- message: 'Invocation deadline exceeded.',
159
- }
160
- : operation.signal.aborted
161
- ? { code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }
162
- : classifyError(input);
155
+ const error = classifyRequestError(input, operation);
163
156
  if (error.code === errors.CODES.INTERNAL_ERROR) {
164
157
  socket.close(1011, 'Internal error');
165
158
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-server",
3
- "version": "0.5.0-beta.65",
3
+ "version": "0.5.0-beta.67",
4
4
  "description": "Portable HTTP and WebSocket boundary for Astrale invocation, publication, installation, and issuer protocols",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -33,9 +33,9 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@astrale-os/kernel-core": "0.9.0-beta.57",
37
- "@astrale-os/kernel-dsl": "0.2.0-beta.41",
38
- "@astrale-os/kernel-protocol": "0.5.0-beta.63",
36
+ "@astrale-os/kernel-core": "0.9.0-beta.59",
37
+ "@astrale-os/kernel-dsl": "0.2.0-beta.43",
38
+ "@astrale-os/kernel-protocol": "0.5.0-beta.65",
39
39
  "hono": "^4.6.20",
40
40
  "msgpackr": "^2.0.5"
41
41
  },