@milaboratories/pframes-rs-serv 1.0.68 → 1.0.70

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/fs-store.cjs CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var node_fs = require('node:fs');
4
3
  var promises = require('node:fs/promises');
5
4
  var node_path = require('node:path');
6
5
  var plModelMiddleLayer = require('@milaboratories/pl-model-middle-layer');
@@ -28,51 +27,60 @@ class FileSystemStore extends plModelMiddleLayer.PFrameInternal.ObjectStore {
28
27
  }
29
28
  async request(filename, params) {
30
29
  let file;
30
+ const respond = async (response) => {
31
+ await params.callback(response).finally(async () => await file?.close());
32
+ };
31
33
  try {
32
- const path = node_path.join(this.rootDir, filename);
33
- file = await promises.open(path, 'r');
34
- }
35
- catch (error) {
36
- this.logger('error', `File system store failed to open file ${filename}: ${plModelCommon.ensureError(error)}`);
37
- return await params.callback({ type: 'NotFound' });
38
- }
39
- try {
34
+ try {
35
+ const path = node_path.join(this.rootDir, filename);
36
+ file = await promises.open(path, 'r');
37
+ }
38
+ catch (error) {
39
+ this.logger('error', `File system store failed to open file ${filename}: ${plModelCommon.ensureError(error)}`);
40
+ return await respond({ type: 'NotFound' });
41
+ }
42
+ params.signal.throwIfAborted();
40
43
  let size;
41
44
  try {
42
45
  ({ size } = await file.stat());
43
46
  }
44
47
  catch (error) {
45
48
  this.logger('error', `File system store failed to get size of file ${filename}: ${plModelCommon.ensureError(error)}`);
46
- return await params.callback({ type: 'InternalError' });
49
+ return await respond({ type: 'InternalError' });
47
50
  }
51
+ params.signal.throwIfAborted();
48
52
  const range = this.translate(size, params.range);
49
53
  if (!range) {
50
- return await params.callback({ type: 'RangeNotSatisfiable', size });
54
+ return await respond({ type: 'RangeNotSatisfiable', size });
51
55
  }
52
56
  if (params.method === 'HEAD') {
53
- return await params.callback({ type: 'Ok', size, range });
57
+ return await respond({ type: 'Ok', size, range });
54
58
  }
55
59
  let data;
56
60
  try {
57
- data = node_fs.createReadStream('ignored', {
58
- fd: file.fd,
59
- autoClose: false,
61
+ data = file.createReadStream({
60
62
  start: range.start,
61
63
  end: range.end,
62
- signal: params.signal
64
+ autoClose: false
63
65
  });
64
66
  this.logger('info', `File system store created read stream for ${filename}[${range.start}..=${range.end}]`);
65
67
  }
66
68
  catch (error) {
67
69
  this.logger('error', `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${plModelCommon.ensureError(error)}`);
68
- return await params.callback({ type: 'InternalError' });
70
+ return await respond({ type: 'InternalError' });
71
+ }
72
+ try {
73
+ return await respond({ type: 'Ok', size, range, data });
74
+ }
75
+ catch (error) {
76
+ this.logger('error', `File system store received unexpected rejection from callback: ${plModelCommon.ensureError(error)}`);
69
77
  }
70
- return await params
71
- .callback({ type: 'Ok', size, range, data })
72
- .catch(() => { });
73
78
  }
74
- finally {
75
- await file.close();
79
+ catch (error) {
80
+ if (!plModelCommon.isAbortError(error)) {
81
+ this.logger('error', `File system store unhandled error: ${plModelCommon.ensureError(error)}`);
82
+ }
83
+ return await respond({ type: 'InternalError' });
76
84
  }
77
85
  }
78
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fs-store.cjs","sources":["../src/fs-store.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport { createReadStream } from 'node:fs';\nimport { stat, open, type FileHandle } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport { ensureError } from '@milaboratories/pl-model-common';\n\n/** Object store for serving files from a local directory */\nexport class FileSystemStore extends PFrameInternal.ObjectStore {\n private readonly rootDir: string;\n\n private constructor(options: PFrameInternal.FsStoreOptions) {\n super(options);\n\n this.rootDir = options.rootDir;\n }\n\n static async init(\n options: PFrameInternal.FsStoreOptions\n ): Promise<FileSystemStore> {\n const resolvedRootDir = resolve(options.rootDir);\n\n const rootStats = await stat(resolvedRootDir).catch(() => {\n throw new Error(\n `File system store root directory does not exist: ${resolvedRootDir}`\n );\n });\n if (!rootStats.isDirectory()) {\n throw new Error(\n `File system store root path is not a directory: ${resolvedRootDir}`\n );\n }\n\n return new FileSystemStore({\n ...options,\n rootDir: resolvedRootDir\n });\n }\n\n override async request(\n filename: PFrameInternal.ParquetFileName,\n params: {\n method: PFrameInternal.HttpMethod;\n range?: PFrameInternal.HttpRange;\n signal: AbortSignal;\n callback: (response: PFrameInternal.ObjectStoreResponse) => Promise<void>;\n }\n ): Promise<void> {\n let file: FileHandle;\n try {\n const path = join(this.rootDir, filename);\n file = await open(path, 'r');\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to open file ${filename}: ${ensureError(error)}`\n );\n return await params.callback({ type: 'NotFound' });\n }\n\n try {\n let size: number;\n try {\n ({ size } = await file.stat());\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to get size of file ${filename}: ${ensureError(error)}`\n );\n return await params.callback({ type: 'InternalError' });\n }\n\n const range = this.translate(size, params.range);\n if (!range) {\n return await params.callback({ type: 'RangeNotSatisfiable', size });\n }\n\n if (params.method === 'HEAD') {\n return await params.callback({ type: 'Ok', size, range });\n }\n\n let data: Readable;\n try {\n data = createReadStream('ignored', {\n fd: file.fd,\n autoClose: false,\n start: range.start,\n end: range.end,\n signal: params.signal\n });\n this.logger(\n 'info',\n `File system store created read stream for ${filename}[${range.start}..=${range.end}]`\n );\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${ensureError(error)}`\n );\n return await params.callback({ type: 'InternalError' });\n }\n\n return await params\n .callback({ type: 'Ok', size, range, data })\n .catch(() => {});\n } finally {\n await file.close();\n }\n }\n}\n"],"names":["PFrameInternal","resolve","stat","join","open","ensureError","createReadStream"],"mappings":"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"}
1
+ {"version":3,"file":"fs-store.cjs","sources":["../src/fs-store.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport { stat, open, type FileHandle } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport { ensureError, isAbortError } from '@milaboratories/pl-model-common';\n\n/** Object store for serving files from a local directory */\nexport class FileSystemStore extends PFrameInternal.ObjectStore {\n private readonly rootDir: string;\n\n private constructor(options: PFrameInternal.FsStoreOptions) {\n super(options);\n\n this.rootDir = options.rootDir;\n }\n\n static async init(\n options: PFrameInternal.FsStoreOptions\n ): Promise<FileSystemStore> {\n const resolvedRootDir = resolve(options.rootDir);\n\n const rootStats = await stat(resolvedRootDir).catch(() => {\n throw new Error(\n `File system store root directory does not exist: ${resolvedRootDir}`\n );\n });\n if (!rootStats.isDirectory()) {\n throw new Error(\n `File system store root path is not a directory: ${resolvedRootDir}`\n );\n }\n\n return new FileSystemStore({\n ...options,\n rootDir: resolvedRootDir\n });\n }\n\n override async request(\n filename: PFrameInternal.ParquetFileName,\n params: {\n method: PFrameInternal.HttpMethod;\n range?: PFrameInternal.HttpRange;\n signal: AbortSignal;\n callback: (response: PFrameInternal.ObjectStoreResponse) => Promise<void>;\n }\n ): Promise<void> {\n let file: FileHandle | undefined;\n const respond = async (response: PFrameInternal.ObjectStoreResponse) => {\n await params.callback(response).finally(async () => await file?.close());\n };\n\n try {\n try {\n const path = join(this.rootDir, filename);\n file = await open(path, 'r');\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to open file ${filename}: ${ensureError(error)}`\n );\n return await respond({ type: 'NotFound' });\n }\n params.signal.throwIfAborted();\n\n let size: number;\n try {\n ({ size } = await file.stat());\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to get size of file ${filename}: ${ensureError(error)}`\n );\n return await respond({ type: 'InternalError' });\n }\n params.signal.throwIfAborted();\n\n const range = this.translate(size, params.range);\n if (!range) {\n return await respond({ type: 'RangeNotSatisfiable', size });\n }\n\n if (params.method === 'HEAD') {\n return await respond({ type: 'Ok', size, range });\n }\n\n let data: Readable;\n try {\n data = file.createReadStream({\n start: range.start,\n end: range.end,\n autoClose: false\n });\n this.logger(\n 'info',\n `File system store created read stream for ${filename}[${range.start}..=${range.end}]`\n );\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${ensureError(error)}`\n );\n return await respond({ type: 'InternalError' });\n }\n\n try {\n return await respond({ type: 'Ok', size, range, data });\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store received unexpected rejection from callback: ${ensureError(error)}`\n );\n }\n } catch (error: unknown) {\n if (!isAbortError(error)) {\n this.logger(\n 'error',\n `File system store unhandled error: ${ensureError(error)}`\n );\n }\n return await respond({ type: 'InternalError' });\n }\n }\n}\n"],"names":["PFrameInternal","resolve","stat","join","open","ensureError","isAbortError"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fs-store.d.ts","sourceRoot":"","sources":["../src/fs-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAGvE,4DAA4D;AAC5D,qBAAa,eAAgB,SAAQ,cAAc,CAAC,WAAW;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,OAAO;WAMM,IAAI,CACf,OAAO,EAAE,cAAc,CAAC,cAAc,GACrC,OAAO,CAAC,eAAe,CAAC;IAoBZ,OAAO,CACpB,QAAQ,EAAE,cAAc,CAAC,eAAe,EACxC,MAAM,EAAE;QACN,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC;QAClC,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC;QACjC,MAAM,EAAE,WAAW,CAAC;QACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC3E,GACA,OAAO,CAAC,IAAI,CAAC;CA8DjB"}
1
+ {"version":3,"file":"fs-store.d.ts","sourceRoot":"","sources":["../src/fs-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAGvE,4DAA4D;AAC5D,qBAAa,eAAgB,SAAQ,cAAc,CAAC,WAAW;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,OAAO;WAMM,IAAI,CACf,OAAO,EAAE,cAAc,CAAC,cAAc,GACrC,OAAO,CAAC,eAAe,CAAC;IAoBZ,OAAO,CACpB,QAAQ,EAAE,cAAc,CAAC,eAAe,EACxC,MAAM,EAAE;QACN,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC;QAClC,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC;QACjC,MAAM,EAAE,WAAW,CAAC;QACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC3E,GACA,OAAO,CAAC,IAAI,CAAC;CA6EjB"}
package/dist/fs-store.js CHANGED
@@ -1,8 +1,7 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { stat, open } from 'node:fs/promises';
3
2
  import { resolve, join } from 'node:path';
4
3
  import { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
5
- import { ensureError } from '@milaboratories/pl-model-common';
4
+ import { ensureError, isAbortError } from '@milaboratories/pl-model-common';
6
5
 
7
6
  /** Object store for serving files from a local directory */
8
7
  class FileSystemStore extends PFrameInternal.ObjectStore {
@@ -26,51 +25,60 @@ class FileSystemStore extends PFrameInternal.ObjectStore {
26
25
  }
27
26
  async request(filename, params) {
28
27
  let file;
28
+ const respond = async (response) => {
29
+ await params.callback(response).finally(async () => await file?.close());
30
+ };
29
31
  try {
30
- const path = join(this.rootDir, filename);
31
- file = await open(path, 'r');
32
- }
33
- catch (error) {
34
- this.logger('error', `File system store failed to open file ${filename}: ${ensureError(error)}`);
35
- return await params.callback({ type: 'NotFound' });
36
- }
37
- try {
32
+ try {
33
+ const path = join(this.rootDir, filename);
34
+ file = await open(path, 'r');
35
+ }
36
+ catch (error) {
37
+ this.logger('error', `File system store failed to open file ${filename}: ${ensureError(error)}`);
38
+ return await respond({ type: 'NotFound' });
39
+ }
40
+ params.signal.throwIfAborted();
38
41
  let size;
39
42
  try {
40
43
  ({ size } = await file.stat());
41
44
  }
42
45
  catch (error) {
43
46
  this.logger('error', `File system store failed to get size of file ${filename}: ${ensureError(error)}`);
44
- return await params.callback({ type: 'InternalError' });
47
+ return await respond({ type: 'InternalError' });
45
48
  }
49
+ params.signal.throwIfAborted();
46
50
  const range = this.translate(size, params.range);
47
51
  if (!range) {
48
- return await params.callback({ type: 'RangeNotSatisfiable', size });
52
+ return await respond({ type: 'RangeNotSatisfiable', size });
49
53
  }
50
54
  if (params.method === 'HEAD') {
51
- return await params.callback({ type: 'Ok', size, range });
55
+ return await respond({ type: 'Ok', size, range });
52
56
  }
53
57
  let data;
54
58
  try {
55
- data = createReadStream('ignored', {
56
- fd: file.fd,
57
- autoClose: false,
59
+ data = file.createReadStream({
58
60
  start: range.start,
59
61
  end: range.end,
60
- signal: params.signal
62
+ autoClose: false
61
63
  });
62
64
  this.logger('info', `File system store created read stream for ${filename}[${range.start}..=${range.end}]`);
63
65
  }
64
66
  catch (error) {
65
67
  this.logger('error', `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${ensureError(error)}`);
66
- return await params.callback({ type: 'InternalError' });
68
+ return await respond({ type: 'InternalError' });
69
+ }
70
+ try {
71
+ return await respond({ type: 'Ok', size, range, data });
72
+ }
73
+ catch (error) {
74
+ this.logger('error', `File system store received unexpected rejection from callback: ${ensureError(error)}`);
67
75
  }
68
- return await params
69
- .callback({ type: 'Ok', size, range, data })
70
- .catch(() => { });
71
76
  }
72
- finally {
73
- await file.close();
77
+ catch (error) {
78
+ if (!isAbortError(error)) {
79
+ this.logger('error', `File system store unhandled error: ${ensureError(error)}`);
80
+ }
81
+ return await respond({ type: 'InternalError' });
74
82
  }
75
83
  }
76
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fs-store.js","sources":["../src/fs-store.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport { createReadStream } from 'node:fs';\nimport { stat, open, type FileHandle } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport { ensureError } from '@milaboratories/pl-model-common';\n\n/** Object store for serving files from a local directory */\nexport class FileSystemStore extends PFrameInternal.ObjectStore {\n private readonly rootDir: string;\n\n private constructor(options: PFrameInternal.FsStoreOptions) {\n super(options);\n\n this.rootDir = options.rootDir;\n }\n\n static async init(\n options: PFrameInternal.FsStoreOptions\n ): Promise<FileSystemStore> {\n const resolvedRootDir = resolve(options.rootDir);\n\n const rootStats = await stat(resolvedRootDir).catch(() => {\n throw new Error(\n `File system store root directory does not exist: ${resolvedRootDir}`\n );\n });\n if (!rootStats.isDirectory()) {\n throw new Error(\n `File system store root path is not a directory: ${resolvedRootDir}`\n );\n }\n\n return new FileSystemStore({\n ...options,\n rootDir: resolvedRootDir\n });\n }\n\n override async request(\n filename: PFrameInternal.ParquetFileName,\n params: {\n method: PFrameInternal.HttpMethod;\n range?: PFrameInternal.HttpRange;\n signal: AbortSignal;\n callback: (response: PFrameInternal.ObjectStoreResponse) => Promise<void>;\n }\n ): Promise<void> {\n let file: FileHandle;\n try {\n const path = join(this.rootDir, filename);\n file = await open(path, 'r');\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to open file ${filename}: ${ensureError(error)}`\n );\n return await params.callback({ type: 'NotFound' });\n }\n\n try {\n let size: number;\n try {\n ({ size } = await file.stat());\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to get size of file ${filename}: ${ensureError(error)}`\n );\n return await params.callback({ type: 'InternalError' });\n }\n\n const range = this.translate(size, params.range);\n if (!range) {\n return await params.callback({ type: 'RangeNotSatisfiable', size });\n }\n\n if (params.method === 'HEAD') {\n return await params.callback({ type: 'Ok', size, range });\n }\n\n let data: Readable;\n try {\n data = createReadStream('ignored', {\n fd: file.fd,\n autoClose: false,\n start: range.start,\n end: range.end,\n signal: params.signal\n });\n this.logger(\n 'info',\n `File system store created read stream for ${filename}[${range.start}..=${range.end}]`\n );\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${ensureError(error)}`\n );\n return await params.callback({ type: 'InternalError' });\n }\n\n return await params\n .callback({ type: 'Ok', size, range, data })\n .catch(() => {});\n } finally {\n await file.close();\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fs-store.js","sources":["../src/fs-store.ts"],"sourcesContent":["import type { Readable } from 'node:stream';\nimport { stat, open, type FileHandle } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport { ensureError, isAbortError } from '@milaboratories/pl-model-common';\n\n/** Object store for serving files from a local directory */\nexport class FileSystemStore extends PFrameInternal.ObjectStore {\n private readonly rootDir: string;\n\n private constructor(options: PFrameInternal.FsStoreOptions) {\n super(options);\n\n this.rootDir = options.rootDir;\n }\n\n static async init(\n options: PFrameInternal.FsStoreOptions\n ): Promise<FileSystemStore> {\n const resolvedRootDir = resolve(options.rootDir);\n\n const rootStats = await stat(resolvedRootDir).catch(() => {\n throw new Error(\n `File system store root directory does not exist: ${resolvedRootDir}`\n );\n });\n if (!rootStats.isDirectory()) {\n throw new Error(\n `File system store root path is not a directory: ${resolvedRootDir}`\n );\n }\n\n return new FileSystemStore({\n ...options,\n rootDir: resolvedRootDir\n });\n }\n\n override async request(\n filename: PFrameInternal.ParquetFileName,\n params: {\n method: PFrameInternal.HttpMethod;\n range?: PFrameInternal.HttpRange;\n signal: AbortSignal;\n callback: (response: PFrameInternal.ObjectStoreResponse) => Promise<void>;\n }\n ): Promise<void> {\n let file: FileHandle | undefined;\n const respond = async (response: PFrameInternal.ObjectStoreResponse) => {\n await params.callback(response).finally(async () => await file?.close());\n };\n\n try {\n try {\n const path = join(this.rootDir, filename);\n file = await open(path, 'r');\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to open file ${filename}: ${ensureError(error)}`\n );\n return await respond({ type: 'NotFound' });\n }\n params.signal.throwIfAborted();\n\n let size: number;\n try {\n ({ size } = await file.stat());\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to get size of file ${filename}: ${ensureError(error)}`\n );\n return await respond({ type: 'InternalError' });\n }\n params.signal.throwIfAborted();\n\n const range = this.translate(size, params.range);\n if (!range) {\n return await respond({ type: 'RangeNotSatisfiable', size });\n }\n\n if (params.method === 'HEAD') {\n return await respond({ type: 'Ok', size, range });\n }\n\n let data: Readable;\n try {\n data = file.createReadStream({\n start: range.start,\n end: range.end,\n autoClose: false\n });\n this.logger(\n 'info',\n `File system store created read stream for ${filename}[${range.start}..=${range.end}]`\n );\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store failed to create read stream for ${filename}[${range.start}..=${range.end}]: ${ensureError(error)}`\n );\n return await respond({ type: 'InternalError' });\n }\n\n try {\n return await respond({ type: 'Ok', size, range, data });\n } catch (error: unknown) {\n this.logger(\n 'error',\n `File system store received unexpected rejection from callback: ${ensureError(error)}`\n );\n }\n } catch (error: unknown) {\n if (!isAbortError(error)) {\n this.logger(\n 'error',\n `File system store unhandled error: ${ensureError(error)}`\n );\n }\n return await respond({ type: 'InternalError' });\n }\n }\n}\n"],"names":[],"mappings":"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"}
package/dist/handler.cjs CHANGED
@@ -9,9 +9,10 @@ var options = require('./utils/options.cjs');
9
9
  var range = require('./utils/range.cjs');
10
10
  var method = require('./utils/method.cjs');
11
11
  var status = require('./utils/status.cjs');
12
+ var plModelCommon = require('@milaboratories/pl-model-common');
12
13
 
13
14
  /** Main request handler for parquet files */
14
- async function handleRequest(request, response, store) {
15
+ function handleRequest(request, response, store) {
15
16
  // RFC 9110 section 6.6.1: Date header should be present in all responses
16
17
  response.sendDate = true;
17
18
  // RFC 9110 section 8.6: Content-Length 0 as default for error responses
@@ -65,8 +66,11 @@ async function handleRequest(request, response, store) {
65
66
  signal,
66
67
  // pipeline automatically destroys the streams if they were not gracefully closed
67
68
  callback: async (result) => {
68
- if (response.destroyed)
69
- return void response.destroy();
69
+ if (request.destroyed) {
70
+ // request has timed out, close the connection
71
+ response.setHeader(headers.HeaderName.Connection, headers.HeaderValue.Connection);
72
+ return void response.writeHead(status.StatusCode.RequestTimeout).end();
73
+ }
70
74
  switch (result.type) {
71
75
  case 'InternalError':
72
76
  // object store encountered network error, retry by client can help
@@ -98,11 +102,13 @@ async function handleRequest(request, response, store) {
98
102
  if (method.isHead(method$1)) {
99
103
  return void response.end();
100
104
  }
101
- return await promises.pipeline(result.data, response, { signal }).catch(() => {
102
- // Pipeline errors are expected when request is aborted or connection is lost
103
- // Response head was already written, so we can't change status code
104
- // Just mute the error - pipeline destroys the response stream
105
- });
105
+ try {
106
+ return await promises.pipeline(result.data, response, { signal });
107
+ }
108
+ catch (error) {
109
+ if (!plModelCommon.isAbortError(error))
110
+ throw error;
111
+ }
106
112
  }
107
113
  });
108
114
  }
@@ -117,7 +123,7 @@ async function handleRequest(request, response, store) {
117
123
  */
118
124
  function createRequestHandler(options) {
119
125
  const { store } = options;
120
- return (request, response) => void handleRequest(request, response, store);
126
+ return (request, response) => handleRequest(request, response, store);
121
127
  }
122
128
  /** Request authorization middleware */
123
129
  function authorizeRequest(request, response, handler, authHeader) {
@@ -1 +1 @@
1
- {"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","timingSafeEqual"],"mappings":"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"}
1
+ {"version":3,"file":"handler.cjs","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\nimport { isAbortError } from '@milaboratories/pl-model-common';\n\n/** Main request handler for parquet files */\nfunction handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (request.destroyed) {\n // request has timed out, close the connection\n response.setHeader(HeaderName.Connection, HeaderValue.Connection);\n return void response.writeHead(StatusCode.RequestTimeout).end();\n }\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n try {\n return await pipeline(result.data!, response, { signal });\n } catch (error: unknown) {\n if (!isAbortError(error)) throw error;\n }\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":["HeaderName","HeaderValue","method","isGetOrHead","StatusCode","filename","getFilenameFromUrl","etag","createETag","options","Options","range","parseRange","isGet","isHead","pipeline","isAbortError","timingSafeEqual"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAuI5E;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,CAAC,qBAAqB,GAC5C,eAAe,CAGjB;AA0CD,yDAAyD;AACzD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,cAAc,CAAC,sBAAsB,GAC/C,eAAe,CAIjB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AA4I5E;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,CAAC,qBAAqB,GAC5C,eAAe,CAGjB;AA0CD,yDAAyD;AACzD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,cAAc,CAAC,sBAAsB,GAC/C,eAAe,CAIjB"}
package/dist/handler.js CHANGED
@@ -7,9 +7,10 @@ import { Options } from './utils/options.js';
7
7
  import { parseRange } from './utils/range.js';
8
8
  import { isGetOrHead, isGet, isHead } from './utils/method.js';
9
9
  import { StatusCode } from './utils/status.js';
10
+ import { isAbortError } from '@milaboratories/pl-model-common';
10
11
 
11
12
  /** Main request handler for parquet files */
12
- async function handleRequest(request, response, store) {
13
+ function handleRequest(request, response, store) {
13
14
  // RFC 9110 section 6.6.1: Date header should be present in all responses
14
15
  response.sendDate = true;
15
16
  // RFC 9110 section 8.6: Content-Length 0 as default for error responses
@@ -63,8 +64,11 @@ async function handleRequest(request, response, store) {
63
64
  signal,
64
65
  // pipeline automatically destroys the streams if they were not gracefully closed
65
66
  callback: async (result) => {
66
- if (response.destroyed)
67
- return void response.destroy();
67
+ if (request.destroyed) {
68
+ // request has timed out, close the connection
69
+ response.setHeader(HeaderName.Connection, HeaderValue.Connection);
70
+ return void response.writeHead(StatusCode.RequestTimeout).end();
71
+ }
68
72
  switch (result.type) {
69
73
  case 'InternalError':
70
74
  // object store encountered network error, retry by client can help
@@ -96,11 +100,13 @@ async function handleRequest(request, response, store) {
96
100
  if (isHead(method)) {
97
101
  return void response.end();
98
102
  }
99
- return await pipeline(result.data, response, { signal }).catch(() => {
100
- // Pipeline errors are expected when request is aborted or connection is lost
101
- // Response head was already written, so we can't change status code
102
- // Just mute the error - pipeline destroys the response stream
103
- });
103
+ try {
104
+ return await pipeline(result.data, response, { signal });
105
+ }
106
+ catch (error) {
107
+ if (!isAbortError(error))
108
+ throw error;
109
+ }
104
110
  }
105
111
  });
106
112
  }
@@ -115,7 +121,7 @@ async function handleRequest(request, response, store) {
115
121
  */
116
122
  function createRequestHandler(options) {
117
123
  const { store } = options;
118
- return (request, response) => void handleRequest(request, response, store);
124
+ return (request, response) => handleRequest(request, response, store);
119
125
  }
120
126
  /** Request authorization middleware */
121
127
  function authorizeRequest(request, response, handler, authHeader) {
@@ -1 +1 @@
1
- {"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\n\n/** Main request handler for parquet files */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): Promise<void> {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (response.destroyed) return void response.destroy();\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n return await pipeline(result.data!, response, { signal }).catch(() => {\n // Pipeline errors are expected when request is aborted or connection is lost\n // Response head was already written, so we can't change status code\n // Just mute the error - pipeline destroys the response stream\n });\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => void handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.js","sources":["../src/handler.ts"],"sourcesContent":["import type {\n IncomingMessage,\n RequestListener,\n ServerResponse\n} from 'node:http';\nimport { pipeline } from 'node:stream/promises';\nimport { timingSafeEqual } from 'node:crypto';\nimport type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';\nimport {\n createETag,\n getFilenameFromUrl,\n parseRange,\n isGetOrHead,\n isGet,\n isHead,\n Options,\n StatusCode,\n HeaderName,\n HeaderValue\n} from './utils';\nimport { isAbortError } from '@milaboratories/pl-model-common';\n\n/** Main request handler for parquet files */\nfunction handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n store: PFrameInternal.ObjectStore\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n // RFC 9110 section 15.5.6: Method not allowed\n const method = request.method;\n if (!isGetOrHead(method)) {\n response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n return void response.writeHead(StatusCode.MethodNotAllowed).end();\n }\n\n const filename = getFilenameFromUrl(request);\n if (filename === null) {\n return void response.writeHead(StatusCode.Gone).end();\n }\n\n // From now on we are sure that the response would be a Parquet file\n response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n const etag = createETag(filename);\n // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n const mtime = new Date(0); // Using fake fixed date since files are immutable\n // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n response.setHeader(HeaderName.ETag, etag);\n response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n const options = new Options(request);\n // RFC 9110 section 13.1.1: If-Match precondition evaluation\n // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n if (options.preconditionFailed(etag, mtime)) {\n return void response.writeHead(StatusCode.PreconditionFailed).end();\n }\n // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n else if (options.notModified(etag, mtime)) {\n return void response.writeHead(StatusCode.NotModified).end();\n }\n\n const range = parseRange(request);\n if (range === null) {\n return void response.writeHead(StatusCode.BadRequest).end();\n }\n\n const abortController = new AbortController();\n request.on('close', () => abortController.abort());\n const signal = abortController.signal;\n\n store.request(filename, {\n method: 'GET',\n range,\n signal,\n // pipeline automatically destroys the streams if they were not gracefully closed\n callback: async (result) => {\n if (request.destroyed) {\n // request has timed out, close the connection\n response.setHeader(HeaderName.Connection, HeaderValue.Connection);\n return void response.writeHead(StatusCode.RequestTimeout).end();\n }\n\n switch (result.type) {\n case 'InternalError':\n // object store encountered network error, retry by client can help\n return void response.writeHead(StatusCode.InternalServerError).end();\n case 'NotFound':\n // RFC 9110 section 15.4.5: Not found\n return void response.writeHead(StatusCode.NotFound).end();\n case 'RangeNotSatisfiable':\n // RFC 9110 section 15.5.17: Range not satisfiable\n response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n case 'Ok':\n break;\n }\n\n if (isGet(method) && !result.data) {\n // object store implementation is incorrect, retry by client cannot help\n return void response.writeHead(StatusCode.GatewayTimeout).end();\n }\n\n if (range) {\n // RFC 9110 section 14.4: Partial content response\n response.setHeader(\n HeaderName.ContentLength,\n result.range.end - result.range.start + 1\n );\n response.setHeader(\n HeaderName.ContentRange,\n `bytes ${result.range.start}-${result.range.end}/${result.size}`\n );\n response.writeHead(StatusCode.PartialContent);\n } else {\n // RFC 9110 section 15.3.1: OK response\n response.setHeader(HeaderName.ContentLength, result.size);\n response.writeHead(StatusCode.Ok);\n }\n\n // RFC 9110 section 9.3.2: HEAD method must not return message body\n if (isHead(method)) {\n return void response.end();\n }\n\n try {\n return await pipeline(result.data!, response, { signal });\n } catch (error: unknown) {\n if (!isAbortError(error)) throw error;\n }\n }\n });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n options: PFrameInternal.RequestHandlerOptions\n): RequestListener {\n const { store } = options;\n return (request, response) => handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n request: IncomingMessage,\n response: ServerResponse,\n handler: RequestListener,\n authHeader: string\n): void {\n // RFC 9110 section 6.6.1: Date header should be present in all responses\n response.sendDate = true;\n // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n response.strictContentLength = true;\n response.setHeader(HeaderName.ContentLength, 0);\n // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n const actualHeader = request.headers[HeaderName.Authorization];\n\n // Early length check to avoid unnecessary processing\n if (!actualHeader || actualHeader.length !== authHeader.length) {\n // RFC 9110 section 11.6.1: WWW-Authenticate header field\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n // Use timing-safe comparison to prevent timing attacks\n // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n const encoder = new TextEncoder();\n const receivedBuffer = encoder.encode(actualHeader);\n const expectedBuffer = encoder.encode(authHeader);\n\n if (\n receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n !timingSafeEqual(receivedBuffer, expectedBuffer)\n ) {\n response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n return void response.writeHead(StatusCode.Unauthorized).end();\n }\n\n return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n handler: RequestListener,\n authToken: PFrameInternal.HttpAuthorizationToken\n): RequestListener {\n const authHeader = `Bearer ${authToken}`;\n return (request, response) =>\n authorizeRequest(request, response, handler, authHeader);\n}\n"],"names":[],"mappings":"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"}