nucleus-core-ts 0.9.925 → 0.9.926

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/.build-ok CHANGED
@@ -1 +1 @@
1
- 0.9.925
1
+ 0.9.926
@@ -1,5 +1,6 @@
1
1
  import { evaluateAccess, ManifestCache, RoleClaimsCache, resolveClaimsFromRoles, verifyJwtHS256 } from './authz';
2
2
  import { createProxyLogger, matchPath, parseCookies, rewritePath } from './utils';
3
+ import { canBufferForRetry, timeoutForBody } from './largeBody';
3
4
  async function fetchSuppressedClaims(url, cookieHeader, logger) {
4
5
  const empty = {
5
6
  suppressedClaims: [],
@@ -461,12 +462,33 @@ export function createHttpProxyHandler(config) {
461
462
  } else if (!accessToken && isAuthPath) {
462
463
  logger.warn(`[Auth:proxy] NO TOKEN available for ${path} — request will likely fail with 401`);
463
464
  }
464
- const timeout = target.timeout ?? 30000;
465
+ const baseTimeout = target.timeout ?? 30000;
466
+ /*
467
+ * A file needs minutes, not thirty seconds.
468
+ *
469
+ * The fixed timeout aborted a 200 MB video long before the last byte, and
470
+ * the browser had no way to tell that from a slow line — the bar simply
471
+ * stopped. The allowance now grows with the declared size.
472
+ */ const timeout = timeoutForBody(req.headers, baseTimeout);
465
473
  const followRedirects = target.followRedirects !== false;
466
474
  // Buffer body for potential retry (only if 401 retry is enabled)
467
475
  let bodyForRetry = null;
468
476
  let bodyToSend = req.body;
469
- if (refreshEnabled && (refreshConfig.retryOn401 ?? true) && req.body) {
477
+ /*
478
+ * Only a body small enough to hold.
479
+ *
480
+ * Buffering exists so a 401 can be answered by refreshing the token and
481
+ * sending the same request again — worth it for a form post, fatal for a
482
+ * file. An 800 MB video was read into an ArrayBuffer inside a container
483
+ * limited to 256 MiB: the upload appeared to run, because the browser
484
+ * really was sending bytes, and then died with nothing to show for it.
485
+ *
486
+ * A large body streams straight through and gives up the retry. That is
487
+ * the right way round: an upload that is refused can be started again by
488
+ * the person who chose the file; an upload that never completes cannot be
489
+ * rescued by anybody.
490
+ */ const bufferable = canBufferForRetry(req.headers);
491
+ if (refreshEnabled && (refreshConfig.retryOn401 ?? true) && req.body && bufferable) {
470
492
  try {
471
493
  bodyForRetry = await req.arrayBuffer();
472
494
  bodyToSend = bodyForRetry;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Whether a request body is small enough to hold in memory for a retry.
3
+ *
4
+ * The proxy buffers the whole body so that a 401 can be answered by refreshing
5
+ * the token and sending the same request again. That is a good trade for a form
6
+ * post and a catastrophic one for a file: an eight-hundred-megabyte video was
7
+ * read into `ArrayBuffer` inside a container limited to 256 MiB, so the upload
8
+ * appeared to run — the browser really was sending bytes — and then died with
9
+ * nothing to show for it. Measured on the customer's install: the video module
10
+ * accepts up to 1 GB, the ingress passes 1 GB, and the proxy in between could
11
+ * never carry more than a fraction of it.
12
+ *
13
+ * A large body is streamed straight through instead. The cost is that it cannot
14
+ * be replayed after a token refresh, which is the right way round: an upload
15
+ * that is refused can be started again by the person who chose the file, while
16
+ * an upload that never completes cannot be rescued by anybody.
17
+ */
18
+ export declare const RETRY_BUFFER_LIMIT_BYTES: number;
19
+ /**
20
+ * `null` when the size is not declared.
21
+ *
22
+ * An undeclared length is treated as LARGE: a chunked upload announces nothing,
23
+ * and guessing "small" there is how the buffer gets filled by the one request
24
+ * that must not fill it.
25
+ */
26
+ export declare function declaredBodySize(headers: Headers): number | null;
27
+ export declare function canBufferForRetry(headers: Headers, limit?: number): boolean;
28
+ /**
29
+ * How long to allow, given what is being carried.
30
+ *
31
+ * Thirty seconds is right for an API call and absurd for a file: a 200 MB video
32
+ * over an office line needs minutes, and the old fixed timeout aborted it long
33
+ * before the last byte. The allowance grows with the declared size — a minute
34
+ * per fifty megabytes — and is never shorter than the configured default.
35
+ */
36
+ export declare function timeoutForBody(headers: Headers, baseTimeoutMs: number): number;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Whether a request body is small enough to hold in memory for a retry.
3
+ *
4
+ * The proxy buffers the whole body so that a 401 can be answered by refreshing
5
+ * the token and sending the same request again. That is a good trade for a form
6
+ * post and a catastrophic one for a file: an eight-hundred-megabyte video was
7
+ * read into `ArrayBuffer` inside a container limited to 256 MiB, so the upload
8
+ * appeared to run — the browser really was sending bytes — and then died with
9
+ * nothing to show for it. Measured on the customer's install: the video module
10
+ * accepts up to 1 GB, the ingress passes 1 GB, and the proxy in between could
11
+ * never carry more than a fraction of it.
12
+ *
13
+ * A large body is streamed straight through instead. The cost is that it cannot
14
+ * be replayed after a token refresh, which is the right way round: an upload
15
+ * that is refused can be started again by the person who chose the file, while
16
+ * an upload that never completes cannot be rescued by anybody.
17
+ */ export const RETRY_BUFFER_LIMIT_BYTES = 2 * 1024 * 1024;
18
+ /**
19
+ * `null` when the size is not declared.
20
+ *
21
+ * An undeclared length is treated as LARGE: a chunked upload announces nothing,
22
+ * and guessing "small" there is how the buffer gets filled by the one request
23
+ * that must not fill it.
24
+ */ export function declaredBodySize(headers) {
25
+ const raw = headers.get('content-length');
26
+ if (!raw) return null;
27
+ const size = Number(raw);
28
+ return Number.isFinite(size) && size >= 0 ? size : null;
29
+ }
30
+ export function canBufferForRetry(headers, limit = RETRY_BUFFER_LIMIT_BYTES) {
31
+ const size = declaredBodySize(headers);
32
+ if (size === null) return false;
33
+ return size <= limit;
34
+ }
35
+ /**
36
+ * How long to allow, given what is being carried.
37
+ *
38
+ * Thirty seconds is right for an API call and absurd for a file: a 200 MB video
39
+ * over an office line needs minutes, and the old fixed timeout aborted it long
40
+ * before the last byte. The allowance grows with the declared size — a minute
41
+ * per fifty megabytes — and is never shorter than the configured default.
42
+ */ export function timeoutForBody(headers, baseTimeoutMs) {
43
+ const size = declaredBodySize(headers);
44
+ if (size === null || size <= RETRY_BUFFER_LIMIT_BYTES) return baseTimeoutMs;
45
+ const perFiftyMb = 60000;
46
+ const allowance = Math.ceil(size / (50 * 1024 * 1024)) * perFiftyMb;
47
+ return Math.max(baseTimeoutMs, allowance);
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.925",
3
+ "version": "0.9.926",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",