blaizejs 0.2.3 → 0.3.0

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/index.js CHANGED
@@ -1,17 +1,26 @@
1
1
  /**
2
- * blaizejs v0.2.3
2
+ * blaizejs v0.3.0
3
3
  * A blazing-fast, TypeScript-first Node.js framework with HTTP/2 support, file-based routing, powerful middleware system, and end-to-end type safety for building modern APIs.
4
4
  *
5
5
  * Copyright (c) 2025 BlaizeJS Contributors
6
6
  * @license MIT
7
7
  */
8
8
 
9
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
- }) : x)(function(x) {
12
- if (typeof require !== "undefined") return require.apply(this, arguments);
13
- throw Error('Dynamic require of "' + x + '" is not supported');
14
- });
9
+ import {
10
+ InternalServerError
11
+ } from "./chunk-QQCQRHXJ.js";
12
+ import {
13
+ ValidationError
14
+ } from "./chunk-SRD3AB6T.js";
15
+ import {
16
+ BlaizeError,
17
+ ErrorSeverity,
18
+ ErrorType,
19
+ __require,
20
+ generateCorrelationId,
21
+ getCurrentCorrelationId,
22
+ isBodyParseError
23
+ } from "./chunk-TCPQMZ23.js";
15
24
 
16
25
  // src/middleware/execute.ts
17
26
  function execute(middleware, ctx, next) {
@@ -391,8 +400,474 @@ function runWithContext(context, callback) {
391
400
  return contextStorage.run(context, callback);
392
401
  }
393
402
 
403
+ // src/upload/multipart-parser.ts
404
+ import { randomUUID } from "node:crypto";
405
+ import { createWriteStream } from "node:fs";
406
+ import { tmpdir } from "node:os";
407
+ import { join as join2 } from "node:path";
408
+ import { Readable } from "node:stream";
409
+
410
+ // src/upload/utils.ts
411
+ var BOUNDARY_REGEX = /boundary=([^;]+)/i;
412
+ var CONTENT_DISPOSITION_REGEX = /Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;[\s\r\n]*filename="([^"]*)")?/i;
413
+ var CONTENT_TYPE_REGEX = /Content-Type:\s*([^\r\n]+)/i;
414
+ var MULTIPART_REGEX = /multipart\/form-data/i;
415
+ function extractBoundary(contentType) {
416
+ const match = contentType.match(BOUNDARY_REGEX);
417
+ if (!match || !match[1]) return null;
418
+ let boundary = match[1].trim();
419
+ if (boundary.startsWith('"') && boundary.endsWith('"')) {
420
+ boundary = boundary.slice(1, -1);
421
+ }
422
+ return boundary || null;
423
+ }
424
+ function parseContentDisposition(headers) {
425
+ const match = headers.match(CONTENT_DISPOSITION_REGEX);
426
+ if (!match || !match[1]) return null;
427
+ return {
428
+ name: match[1],
429
+ filename: match[2] !== void 0 ? match[2] : void 0
430
+ };
431
+ }
432
+ function parseContentType(headers) {
433
+ const match = headers.match(CONTENT_TYPE_REGEX);
434
+ return match && match[1]?.trim() ? match[1].trim() : "application/octet-stream";
435
+ }
436
+ function isMultipartContent(contentType) {
437
+ return MULTIPART_REGEX.test(contentType);
438
+ }
439
+
440
+ // src/upload/multipart-parser.ts
441
+ var DEFAULT_OPTIONS = {
442
+ maxFileSize: 10 * 1024 * 1024,
443
+ // 10MB
444
+ maxFiles: 10,
445
+ maxFieldSize: 1 * 1024 * 1024,
446
+ // 1MB
447
+ allowedMimeTypes: [],
448
+ allowedExtensions: [],
449
+ strategy: "stream",
450
+ tempDir: tmpdir(),
451
+ computeHash: false
452
+ };
453
+ function createParserState(boundary, options = {}) {
454
+ return {
455
+ boundary: Buffer.from(`--${boundary}`),
456
+ options: { ...DEFAULT_OPTIONS, ...options },
457
+ fields: /* @__PURE__ */ new Map(),
458
+ files: /* @__PURE__ */ new Map(),
459
+ buffer: Buffer.alloc(0),
460
+ stage: "boundary",
461
+ currentHeaders: "",
462
+ currentField: null,
463
+ currentFilename: void 0,
464
+ currentMimetype: "application/octet-stream",
465
+ currentContentLength: 0,
466
+ fileCount: 0,
467
+ fieldCount: 0,
468
+ currentBufferChunks: [],
469
+ currentStream: null,
470
+ currentTempPath: null,
471
+ currentWriteStream: null,
472
+ streamController: null,
473
+ cleanupTasks: [],
474
+ // Track validation state
475
+ hasFoundValidBoundary: false,
476
+ hasProcessedAnyPart: false,
477
+ isFinished: false
478
+ };
479
+ }
480
+ async function processChunk(state, chunk) {
481
+ const newBuffer = Buffer.concat([state.buffer, chunk]);
482
+ let currentState = { ...state, buffer: newBuffer };
483
+ while (currentState.buffer.length > 0 && !currentState.isFinished) {
484
+ const nextState = await processCurrentStage(currentState);
485
+ if (nextState === currentState) break;
486
+ currentState = nextState;
487
+ }
488
+ return currentState;
489
+ }
490
+ async function processCurrentStage(state) {
491
+ switch (state.stage) {
492
+ case "boundary":
493
+ return processBoundary(state);
494
+ case "headers":
495
+ return processHeaders(state);
496
+ case "content":
497
+ return processContent(state);
498
+ default: {
499
+ const { InternalServerError: InternalServerError2 } = await import("./internal-server-error-CVRDTBLL.js");
500
+ throw new InternalServerError2(`Invalid parser stage`, {
501
+ operation: state.stage
502
+ });
503
+ }
504
+ }
505
+ }
506
+ function processBoundary(state) {
507
+ const boundaryIndex = state.buffer.indexOf(state.boundary);
508
+ if (boundaryIndex === -1) return state;
509
+ const hasFoundValidBoundary = true;
510
+ let buffer = state.buffer.subarray(boundaryIndex + state.boundary.length);
511
+ if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("--"))) {
512
+ return {
513
+ ...state,
514
+ buffer,
515
+ hasFoundValidBoundary,
516
+ isFinished: true,
517
+ stage: "boundary"
518
+ };
519
+ }
520
+ if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("\r\n"))) {
521
+ buffer = buffer.subarray(2);
522
+ }
523
+ return {
524
+ ...state,
525
+ buffer,
526
+ hasFoundValidBoundary,
527
+ stage: "headers",
528
+ currentHeaders: ""
529
+ };
530
+ }
531
+ async function processHeaders(state) {
532
+ const headerEnd = state.buffer.indexOf("\r\n\r\n");
533
+ if (headerEnd === -1) return state;
534
+ const headers = state.buffer.subarray(0, headerEnd).toString("utf8");
535
+ const buffer = state.buffer.subarray(headerEnd + 4);
536
+ const disposition = parseContentDisposition(headers);
537
+ if (!disposition) {
538
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
539
+ throw new ValidationError2("Missing or invalid Content-Disposition header");
540
+ }
541
+ const mimetype = parseContentType(headers);
542
+ const isFile = disposition.filename !== void 0;
543
+ if (isFile && state.fileCount >= state.options.maxFiles) {
544
+ const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
545
+ throw new PayloadTooLargeError("Too many files in upload", {
546
+ fileCount: state.fileCount + 1,
547
+ maxFiles: state.options.maxFiles,
548
+ filename: disposition.filename
549
+ });
550
+ }
551
+ if (isFile && state.options.allowedMimeTypes.length > 0 && !state.options.allowedMimeTypes.includes(mimetype)) {
552
+ const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
553
+ throw new UnsupportedMediaTypeError("File type not allowed", {
554
+ receivedMimeType: mimetype,
555
+ allowedMimeTypes: state.options.allowedMimeTypes,
556
+ filename: disposition.filename
557
+ });
558
+ }
559
+ return {
560
+ ...state,
561
+ buffer,
562
+ stage: "content",
563
+ currentHeaders: headers,
564
+ currentField: disposition.name,
565
+ currentFilename: disposition.filename,
566
+ currentMimetype: mimetype,
567
+ currentContentLength: 0,
568
+ fileCount: isFile ? state.fileCount + 1 : state.fileCount,
569
+ fieldCount: isFile ? state.fieldCount : state.fieldCount + 1,
570
+ currentBufferChunks: []
571
+ };
572
+ }
573
+ async function processContent(state) {
574
+ const nextBoundaryIndex = state.buffer.indexOf(state.boundary);
575
+ let contentChunk;
576
+ let isComplete = false;
577
+ let buffer = state.buffer;
578
+ if (nextBoundaryIndex === -1) {
579
+ const safeLength = Math.max(0, state.buffer.length - state.boundary.length);
580
+ if (safeLength === 0) return state;
581
+ contentChunk = state.buffer.subarray(0, safeLength);
582
+ buffer = state.buffer.subarray(safeLength);
583
+ } else {
584
+ const contentEnd = Math.max(0, nextBoundaryIndex - 2);
585
+ contentChunk = state.buffer.subarray(0, contentEnd);
586
+ buffer = state.buffer.subarray(nextBoundaryIndex);
587
+ isComplete = true;
588
+ }
589
+ let updatedState = { ...state, buffer };
590
+ if (contentChunk.length > 0) {
591
+ updatedState = await processContentChunk(updatedState, contentChunk);
592
+ }
593
+ if (isComplete) {
594
+ updatedState = await finalizeCurrentPart(updatedState);
595
+ updatedState = {
596
+ ...updatedState,
597
+ stage: "boundary",
598
+ hasProcessedAnyPart: true
599
+ // Mark that we've processed at least one part
600
+ };
601
+ }
602
+ return updatedState;
603
+ }
604
+ async function processContentChunk(state, chunk) {
605
+ const newContentLength = state.currentContentLength + chunk.length;
606
+ const maxSize = state.currentFilename !== void 0 ? state.options.maxFileSize : state.options.maxFieldSize;
607
+ if (newContentLength > maxSize) {
608
+ const isFile = state.currentFilename !== void 0;
609
+ const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
610
+ const payloadErrorDetals = state.currentField ? {
611
+ contentType: isFile ? "file" : "field",
612
+ currentSize: newContentLength,
613
+ maxSize,
614
+ field: state.currentField,
615
+ filename: state.currentFilename
616
+ } : {
617
+ contentType: isFile ? "file" : "field",
618
+ currentSize: newContentLength,
619
+ maxSize,
620
+ filename: state.currentFilename
621
+ };
622
+ throw new PayloadTooLargeError(
623
+ `${isFile ? "File" : "Field"} size exceeds limit`,
624
+ payloadErrorDetals
625
+ );
626
+ }
627
+ if (state.currentFilename !== void 0) {
628
+ return processFileChunk(state, chunk, newContentLength);
629
+ } else {
630
+ return {
631
+ ...state,
632
+ currentContentLength: newContentLength,
633
+ currentBufferChunks: [...state.currentBufferChunks, chunk]
634
+ };
635
+ }
636
+ }
637
+ async function processFileChunk(state, chunk, newContentLength) {
638
+ switch (state.options.strategy) {
639
+ case "memory":
640
+ return {
641
+ ...state,
642
+ currentContentLength: newContentLength,
643
+ currentBufferChunks: [...state.currentBufferChunks, chunk]
644
+ };
645
+ case "stream":
646
+ if (state.streamController) {
647
+ state.streamController.enqueue(chunk);
648
+ }
649
+ return { ...state, currentContentLength: newContentLength };
650
+ case "temp":
651
+ if (state.currentWriteStream) {
652
+ await writeToStream(state.currentWriteStream, chunk);
653
+ }
654
+ return { ...state, currentContentLength: newContentLength };
655
+ default: {
656
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
657
+ throw new ValidationError2(`Invalid parsing strategy`);
658
+ }
659
+ }
660
+ }
661
+ async function initializeFileProcessing(state) {
662
+ if (state.currentFilename === void 0) return state;
663
+ switch (state.options.strategy) {
664
+ case "memory":
665
+ return { ...state, currentBufferChunks: [] };
666
+ case "stream": {
667
+ let streamController = null;
668
+ const stream = new ReadableStream({
669
+ start: (controller) => {
670
+ streamController = controller;
671
+ }
672
+ });
673
+ return {
674
+ ...state,
675
+ currentStream: stream,
676
+ // Type cast for Node.js compatibility
677
+ streamController
678
+ };
679
+ }
680
+ case "temp": {
681
+ const tempPath = join2(state.options.tempDir, `upload-${randomUUID()}`);
682
+ const writeStream = createWriteStream(tempPath);
683
+ const cleanupTask = async () => {
684
+ try {
685
+ const { unlink } = await import("node:fs/promises");
686
+ await unlink(tempPath);
687
+ } catch (error) {
688
+ console.warn(`Failed to cleanup temp file: ${tempPath}`, error);
689
+ }
690
+ };
691
+ return {
692
+ ...state,
693
+ currentTempPath: tempPath,
694
+ currentWriteStream: writeStream,
695
+ cleanupTasks: [...state.cleanupTasks, cleanupTask]
696
+ };
697
+ }
698
+ default: {
699
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
700
+ throw new ValidationError2(`Invalid file processing strategy`);
701
+ }
702
+ }
703
+ }
704
+ async function finalizeCurrentPart(state) {
705
+ if (!state.currentField) return resetCurrentPart(state);
706
+ if (state.currentFilename !== void 0) {
707
+ return finalizeFile(state);
708
+ } else {
709
+ return finalizeField(state);
710
+ }
711
+ }
712
+ async function finalizeFile(state) {
713
+ if (!state.currentField || state.currentFilename === void 0) {
714
+ return resetCurrentPart(state);
715
+ }
716
+ let stream;
717
+ let buffer;
718
+ let tempPath;
719
+ switch (state.options.strategy) {
720
+ case "memory":
721
+ buffer = Buffer.concat(state.currentBufferChunks);
722
+ stream = Readable.from(buffer);
723
+ break;
724
+ case "stream":
725
+ if (state.streamController) {
726
+ state.streamController.close();
727
+ }
728
+ stream = state.currentStream;
729
+ break;
730
+ case "temp":
731
+ if (state.currentWriteStream) {
732
+ await closeStream(state.currentWriteStream);
733
+ }
734
+ tempPath = state.currentTempPath;
735
+ stream = Readable.from(Buffer.alloc(0));
736
+ break;
737
+ default: {
738
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
739
+ throw new ValidationError2(`Invalid file finalization strategy`);
740
+ }
741
+ }
742
+ const file = {
743
+ filename: state.currentFilename,
744
+ fieldname: state.currentField,
745
+ mimetype: state.currentMimetype,
746
+ size: state.currentContentLength,
747
+ stream,
748
+ buffer,
749
+ tempPath
750
+ };
751
+ const updatedFiles = addToCollection(state.files, state.currentField, file);
752
+ return {
753
+ ...resetCurrentPart(state),
754
+ files: updatedFiles
755
+ };
756
+ }
757
+ function finalizeField(state) {
758
+ if (!state.currentField) return resetCurrentPart(state);
759
+ const value = Buffer.concat(state.currentBufferChunks).toString("utf8");
760
+ const updatedFields = addToCollection(state.fields, state.currentField, value);
761
+ return {
762
+ ...resetCurrentPart(state),
763
+ fields: updatedFields
764
+ };
765
+ }
766
+ function resetCurrentPart(state) {
767
+ return {
768
+ ...state,
769
+ currentField: null,
770
+ currentFilename: void 0,
771
+ currentContentLength: 0,
772
+ currentBufferChunks: [],
773
+ currentStream: null,
774
+ streamController: null,
775
+ currentTempPath: null,
776
+ currentWriteStream: null
777
+ };
778
+ }
779
+ function addToCollection(collection, key, value) {
780
+ const newCollection = new Map(collection);
781
+ const existing = newCollection.get(key) || [];
782
+ newCollection.set(key, [...existing, value]);
783
+ return newCollection;
784
+ }
785
+ async function finalize(state) {
786
+ if (!state.hasFoundValidBoundary) {
787
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
788
+ throw new ValidationError2("No valid multipart boundary found");
789
+ }
790
+ if (state.hasFoundValidBoundary && !state.hasProcessedAnyPart) {
791
+ const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
792
+ throw new ValidationError2("Empty multipart request");
793
+ }
794
+ const fields = {};
795
+ for (const [key, values] of state.fields.entries()) {
796
+ fields[key] = values.length === 1 ? values[0] : values;
797
+ }
798
+ const files = {};
799
+ for (const [key, fileList] of state.files.entries()) {
800
+ files[key] = fileList.length === 1 ? fileList[0] : fileList;
801
+ }
802
+ return { fields, files };
803
+ }
804
+ async function cleanup(state) {
805
+ await Promise.allSettled(state.cleanupTasks.map((task) => task()));
806
+ if (state.streamController) {
807
+ state.streamController.close();
808
+ }
809
+ if (state.currentWriteStream) {
810
+ await closeStream(state.currentWriteStream);
811
+ }
812
+ }
813
+ async function writeToStream(stream, chunk) {
814
+ return new Promise((resolve3, reject) => {
815
+ stream.write(chunk, (error) => {
816
+ if (error) reject(error);
817
+ else resolve3();
818
+ });
819
+ });
820
+ }
821
+ async function closeStream(stream) {
822
+ return new Promise((resolve3) => {
823
+ stream.end(() => resolve3());
824
+ });
825
+ }
826
+ async function parseMultipartRequest(request, options = {}) {
827
+ const contentType = request.headers["content-type"] || "";
828
+ const boundary = extractBoundary(contentType);
829
+ if (!boundary) {
830
+ const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
831
+ throw new UnsupportedMediaTypeError("Missing boundary in multipart content-type", {
832
+ receivedContentType: contentType,
833
+ expectedFormat: "multipart/form-data; boundary=..."
834
+ });
835
+ }
836
+ let state = createParserState(boundary, options);
837
+ if (state.currentFilename !== void 0) {
838
+ state = await initializeFileProcessing(state);
839
+ }
840
+ try {
841
+ for await (const chunk of request) {
842
+ state = await processChunk(state, chunk);
843
+ }
844
+ return finalize(state);
845
+ } finally {
846
+ await cleanup(state);
847
+ }
848
+ }
849
+
394
850
  // src/context/create.ts
395
851
  var CONTENT_TYPE_HEADER = "Content-Type";
852
+ var DEFAULT_BODY_LIMITS = {
853
+ json: 512 * 1024,
854
+ // 512KB - Most APIs should be much smaller
855
+ form: 1024 * 1024,
856
+ // 1MB - Reasonable for form submissions
857
+ text: 5 * 1024 * 1024,
858
+ // 5MB - Documents, logs, code files
859
+ multipart: {
860
+ maxFileSize: 50 * 1024 * 1024,
861
+ // 50MB per file
862
+ maxTotalSize: 100 * 1024 * 1024,
863
+ // 100MB total request
864
+ maxFiles: 10,
865
+ maxFieldSize: 1024 * 1024
866
+ // 1MB for form fields
867
+ },
868
+ raw: 10 * 1024 * 1024
869
+ // 10MB for unknown content types
870
+ };
396
871
  function parseRequestUrl(req) {
397
872
  const originalUrl = req.url || "/";
398
873
  const host = req.headers.host || "localhost";
@@ -457,7 +932,7 @@ async function createContext(req, res, options = {}) {
457
932
  };
458
933
  ctx.response = createResponseObject(res, responseState, ctx);
459
934
  if (options.parseBody) {
460
- await parseBodyIfNeeded(req, ctx);
935
+ await parseBodyIfNeeded(req, ctx, options);
461
936
  }
462
937
  return ctx;
463
938
  }
@@ -633,34 +1108,60 @@ function createStreamResponder(res, responseState) {
633
1108
  });
634
1109
  };
635
1110
  }
636
- async function parseBodyIfNeeded(req, ctx) {
1111
+ async function parseBodyIfNeeded(req, ctx, options = {}) {
637
1112
  if (shouldSkipParsing(req.method)) {
638
1113
  return;
639
1114
  }
640
1115
  const contentType = req.headers["content-type"] || "";
641
1116
  const contentLength = parseInt(req.headers["content-length"] || "0", 10);
642
- if (contentLength === 0 || contentLength > 1048576) {
1117
+ if (contentLength === 0) {
643
1118
  return;
644
1119
  }
1120
+ const limits = {
1121
+ json: options.bodyLimits?.json ?? DEFAULT_BODY_LIMITS.json,
1122
+ form: options.bodyLimits?.form ?? DEFAULT_BODY_LIMITS.form,
1123
+ text: options.bodyLimits?.text ?? DEFAULT_BODY_LIMITS.text,
1124
+ raw: options.bodyLimits?.raw ?? DEFAULT_BODY_LIMITS.raw,
1125
+ multipart: {
1126
+ maxFileSize: options.bodyLimits?.multipart?.maxFileSize ?? DEFAULT_BODY_LIMITS.multipart.maxFileSize,
1127
+ maxFiles: options.bodyLimits?.multipart?.maxFiles ?? DEFAULT_BODY_LIMITS.multipart.maxFiles,
1128
+ maxFieldSize: options.bodyLimits?.multipart?.maxFieldSize ?? DEFAULT_BODY_LIMITS.multipart.maxFieldSize,
1129
+ maxTotalSize: options.bodyLimits?.multipart?.maxTotalSize ?? DEFAULT_BODY_LIMITS.multipart.maxTotalSize
1130
+ }
1131
+ };
645
1132
  try {
646
- await parseBodyByContentType(req, ctx, contentType);
1133
+ if (contentType.includes("application/json")) {
1134
+ if (contentLength > limits.json) {
1135
+ throw new Error(`JSON body too large: ${contentLength} > ${limits.json} bytes`);
1136
+ }
1137
+ await parseJsonBody(req, ctx);
1138
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
1139
+ if (contentLength > limits.form) {
1140
+ throw new Error(`Form body too large: ${contentLength} > ${limits.form} bytes`);
1141
+ }
1142
+ await parseFormUrlEncodedBody(req, ctx);
1143
+ } else if (contentType.includes("text/")) {
1144
+ if (contentLength > limits.text) {
1145
+ throw new Error(`Text body too large: ${contentLength} > ${limits.text} bytes`);
1146
+ }
1147
+ await parseTextBody(req, ctx);
1148
+ } else if (isMultipartContent(contentType)) {
1149
+ await parseMultipartBody(req, ctx, limits.multipart);
1150
+ } else {
1151
+ if (contentLength > limits.raw) {
1152
+ throw new Error(`Request body too large: ${contentLength} > ${limits.raw} bytes`);
1153
+ }
1154
+ return;
1155
+ }
647
1156
  } catch (error) {
648
- setBodyError(ctx, "body_read_error", "Error reading request body", error);
1157
+ const errorType = contentType.includes("multipart") ? "multipart_parse_error" : "body_read_error";
1158
+ setBodyError(ctx, errorType, "Error reading request body", error);
649
1159
  }
650
1160
  }
651
1161
  function shouldSkipParsing(method) {
652
1162
  const skipMethods = ["GET", "HEAD", "OPTIONS"];
653
1163
  return skipMethods.includes(method || "GET");
654
1164
  }
655
- async function parseBodyByContentType(req, ctx, contentType) {
656
- if (contentType.includes("application/json")) {
657
- await parseJsonBody(req, ctx);
658
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
659
- await parseFormUrlEncodedBody(req, ctx);
660
- } else if (contentType.includes("text/")) {
661
- await parseTextBody(req, ctx);
662
- }
663
- }
664
1165
  async function parseJsonBody(req, ctx) {
665
1166
  const body = await readRequestBody(req);
666
1167
  if (!body) {
@@ -712,8 +1213,27 @@ async function parseTextBody(req, ctx) {
712
1213
  ctx.request.body = body;
713
1214
  }
714
1215
  }
1216
+ async function parseMultipartBody(req, ctx, multipartLimits) {
1217
+ try {
1218
+ const limits = multipartLimits || DEFAULT_BODY_LIMITS.multipart;
1219
+ const multipartData = await parseMultipartRequest(req, {
1220
+ strategy: "stream",
1221
+ maxFileSize: limits.maxFileSize,
1222
+ maxFiles: limits.maxFiles,
1223
+ maxFieldSize: limits.maxFieldSize
1224
+ // Could add total size validation here
1225
+ });
1226
+ ctx.request.multipart = multipartData;
1227
+ ctx.request.files = multipartData.files;
1228
+ ctx.request.body = multipartData.fields;
1229
+ } catch (error) {
1230
+ ctx.request.body = null;
1231
+ setBodyError(ctx, "multipart_parse_error", "Failed to parse multipart data", error);
1232
+ }
1233
+ }
715
1234
  function setBodyError(ctx, type, message, error) {
716
- ctx.state._bodyError = { type, message, error };
1235
+ const bodyError = { type, message, error };
1236
+ ctx.state._bodyError = bodyError;
717
1237
  }
718
1238
  async function readRequestBody(req) {
719
1239
  return new Promise((resolve3, reject) => {
@@ -730,6 +1250,102 @@ async function readRequestBody(req) {
730
1250
  });
731
1251
  }
732
1252
 
1253
+ // src/errors/not-found-error.ts
1254
+ var NotFoundError = class extends BlaizeError {
1255
+ /**
1256
+ * Creates a new NotFoundError instance
1257
+ *
1258
+ * @param title - Human-readable error message
1259
+ * @param details - Optional context about the missing resource
1260
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
1261
+ */
1262
+ constructor(title, details = void 0, correlationId = void 0) {
1263
+ super(
1264
+ "NOT_FOUND" /* NOT_FOUND */,
1265
+ title,
1266
+ 404,
1267
+ // HTTP 404 Not Found
1268
+ correlationId ?? getCurrentCorrelationId(),
1269
+ details
1270
+ );
1271
+ }
1272
+ };
1273
+
1274
+ // src/errors/boundary.ts
1275
+ function isHandledError(error) {
1276
+ return error instanceof BlaizeError;
1277
+ }
1278
+ function formatErrorResponse(error) {
1279
+ if (isHandledError(error)) {
1280
+ return {
1281
+ type: error.type,
1282
+ title: error.title,
1283
+ status: error.status,
1284
+ correlationId: error.correlationId,
1285
+ timestamp: error.timestamp.toISOString(),
1286
+ details: error.details
1287
+ };
1288
+ }
1289
+ const correlationId = generateCorrelationId();
1290
+ let originalMessage;
1291
+ if (error instanceof Error) {
1292
+ originalMessage = error.message;
1293
+ } else if (error === null || error === void 0) {
1294
+ originalMessage = "Unknown error occurred";
1295
+ } else {
1296
+ originalMessage = String(error);
1297
+ }
1298
+ const wrappedError = new InternalServerError(
1299
+ "Internal Server Error",
1300
+ { originalMessage },
1301
+ correlationId
1302
+ );
1303
+ return {
1304
+ type: wrappedError.type,
1305
+ title: wrappedError.title,
1306
+ status: wrappedError.status,
1307
+ correlationId: wrappedError.correlationId,
1308
+ timestamp: wrappedError.timestamp.toISOString(),
1309
+ details: wrappedError.details
1310
+ };
1311
+ }
1312
+ function extractOrGenerateCorrelationId(headerGetter) {
1313
+ return headerGetter("x-correlation-id") ?? generateCorrelationId();
1314
+ }
1315
+ function setErrorResponseHeaders(headerSetter, correlationId) {
1316
+ headerSetter("x-correlation-id", correlationId);
1317
+ }
1318
+
1319
+ // src/middleware/error-boundary.ts
1320
+ function createErrorBoundary(options = {}) {
1321
+ const { debug = false } = options;
1322
+ const middlewareFn = async (ctx, next) => {
1323
+ try {
1324
+ await next();
1325
+ } catch (error) {
1326
+ if (ctx.response.sent) {
1327
+ if (debug) {
1328
+ console.error("Error occurred after response was sent:", error);
1329
+ }
1330
+ return;
1331
+ }
1332
+ if (debug) {
1333
+ console.error("Error boundary caught error:", error);
1334
+ }
1335
+ const correlationId = extractOrGenerateCorrelationId(ctx.request.header);
1336
+ const errorResponse = formatErrorResponse(error);
1337
+ errorResponse.correlationId = correlationId;
1338
+ setErrorResponseHeaders(ctx.response.header, correlationId);
1339
+ ctx.response.status(errorResponse.status).json(errorResponse);
1340
+ }
1341
+ };
1342
+ return {
1343
+ name: "ErrorBoundary",
1344
+ execute: middlewareFn,
1345
+ debug
1346
+ };
1347
+ }
1348
+
733
1349
  // src/server/request-handler.ts
734
1350
  function createRequestHandler(serverInstance) {
735
1351
  return async (req, res) => {
@@ -738,32 +1354,20 @@ function createRequestHandler(serverInstance) {
738
1354
  parseBody: true
739
1355
  // Enable automatic body parsing
740
1356
  });
741
- const handler = compose(serverInstance.middleware);
1357
+ const errorBoundary = createErrorBoundary();
1358
+ const allMiddleware = [errorBoundary, ...serverInstance.middleware];
1359
+ const handler = compose(allMiddleware);
742
1360
  await runWithContext(context, async () => {
743
- try {
744
- await handler(context, async () => {
1361
+ await handler(context, async () => {
1362
+ if (!context.response.sent) {
1363
+ await serverInstance.router.handleRequest(context);
745
1364
  if (!context.response.sent) {
746
- await serverInstance.router.handleRequest(context);
747
- if (!context.response.sent) {
748
- context.response.status(404).json({
749
- error: "Not Found",
750
- message: `Route not found: ${context.request.method} ${context.request.path}`
751
- });
752
- }
1365
+ throw new NotFoundError(
1366
+ `Route not found: ${context.request.method} ${context.request.path}`
1367
+ );
753
1368
  }
754
- });
755
- } catch (error) {
756
- console.error("Error processing request:", error);
757
- if (!context.response.sent) {
758
- context.response.json(
759
- {
760
- error: "Internal Server Error",
761
- message: process.env.NODE_ENV === "development" ? error || "Unknown error" : "An error occurred processing your request"
762
- },
763
- 500
764
- );
765
1369
  }
766
- }
1370
+ });
767
1371
  });
768
1372
  } catch (error) {
769
1373
  console.error("Error creating context:", error);
@@ -1604,81 +2208,8 @@ function watchRoutes(routesDir, options = {}) {
1604
2208
  };
1605
2209
  }
1606
2210
 
1607
- // src/router/handlers/error.ts
1608
- function handleRouteError(ctx, error, options = {}) {
1609
- if (options.log) {
1610
- console.error("Route error:", error);
1611
- }
1612
- const status = getErrorStatus(error);
1613
- const response = {
1614
- error: getErrorType(error),
1615
- message: getErrorMessage(error)
1616
- };
1617
- if (options.detailed) {
1618
- if (error instanceof Error) {
1619
- response.stack = error.stack;
1620
- }
1621
- if (error && typeof error === "object" && "details" in error && error.details) {
1622
- response.details = error.details;
1623
- }
1624
- }
1625
- ctx.response.status(status).json(response);
1626
- }
1627
- function getErrorStatus(error) {
1628
- if (error && typeof error === "object") {
1629
- if ("status" in error && typeof error.status === "number") {
1630
- return error.status;
1631
- }
1632
- if ("statusCode" in error && typeof error.statusCode === "number") {
1633
- return error.statusCode;
1634
- }
1635
- if ("code" in error && typeof error.code === "string") {
1636
- return getStatusFromCode(error.code);
1637
- }
1638
- }
1639
- return 500;
1640
- }
1641
- function getStatusFromCode(code) {
1642
- switch (code) {
1643
- case "NOT_FOUND":
1644
- return 404;
1645
- case "UNAUTHORIZED":
1646
- return 401;
1647
- case "FORBIDDEN":
1648
- return 403;
1649
- case "BAD_REQUEST":
1650
- return 400;
1651
- case "CONFLICT":
1652
- return 409;
1653
- default:
1654
- return 500;
1655
- }
1656
- }
1657
- function getErrorType(error) {
1658
- if (error && typeof error === "object") {
1659
- if ("type" in error && typeof error.type === "string") {
1660
- return error.type;
1661
- }
1662
- if ("name" in error && typeof error.name === "string") {
1663
- return error.name;
1664
- }
1665
- if (error instanceof Error) {
1666
- return error.constructor.name;
1667
- }
1668
- }
1669
- return "Error";
1670
- }
1671
- function getErrorMessage(error) {
1672
- if (error instanceof Error) {
1673
- return error.message;
1674
- }
1675
- if (error && typeof error === "object") {
1676
- if ("message" in error && typeof error.message === "string") {
1677
- return error.message;
1678
- }
1679
- }
1680
- return String(error);
1681
- }
2211
+ // src/router/validation/schema.ts
2212
+ import { z as z6 } from "zod";
1682
2213
 
1683
2214
  // src/router/validation/body.ts
1684
2215
  import { z as z2 } from "zod";
@@ -1719,35 +2250,45 @@ function validateResponse(response, schema) {
1719
2250
  // src/router/validation/schema.ts
1720
2251
  function createRequestValidator(schema, debug = false) {
1721
2252
  const middlewareFn = async (ctx, next) => {
1722
- const errors = {};
1723
2253
  if (schema.params && ctx.request.params) {
1724
2254
  try {
1725
2255
  ctx.request.params = validateParams(ctx.request.params, schema.params);
1726
2256
  } catch (error) {
1727
- errors.params = formatValidationError(error);
2257
+ const fieldErrors = extractZodFieldErrors(error);
2258
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2259
+ throw new ValidationError("Request validation failed", {
2260
+ fields: fieldErrors,
2261
+ errorCount,
2262
+ section: "params"
2263
+ });
1728
2264
  }
1729
2265
  }
1730
2266
  if (schema.query && ctx.request.query) {
1731
2267
  try {
1732
2268
  ctx.request.query = validateQuery(ctx.request.query, schema.query);
1733
2269
  } catch (error) {
1734
- errors.query = formatValidationError(error);
2270
+ const fieldErrors = extractZodFieldErrors(error);
2271
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2272
+ throw new ValidationError("Request validation failed", {
2273
+ fields: fieldErrors,
2274
+ errorCount,
2275
+ section: "query"
2276
+ });
1735
2277
  }
1736
2278
  }
1737
2279
  if (schema.body) {
1738
2280
  try {
1739
2281
  ctx.request.body = validateBody(ctx.request.body, schema.body);
1740
2282
  } catch (error) {
1741
- errors.body = formatValidationError(error);
2283
+ const fieldErrors = extractZodFieldErrors(error);
2284
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2285
+ throw new ValidationError("Request validation failed", {
2286
+ fields: fieldErrors,
2287
+ errorCount,
2288
+ section: "body"
2289
+ });
1742
2290
  }
1743
2291
  }
1744
- if (Object.keys(errors).length > 0) {
1745
- ctx.response.status(400).json({
1746
- error: "Validation Error",
1747
- details: errors
1748
- });
1749
- return;
1750
- }
1751
2292
  await next();
1752
2293
  };
1753
2294
  return {
@@ -1766,12 +2307,11 @@ function createResponseValidator(responseSchema, debug = false) {
1766
2307
  return originalJson.call(ctx.response, validatedBody, status);
1767
2308
  } catch (error) {
1768
2309
  ctx.response.json = originalJson;
1769
- console.error("Response validation error:", error);
1770
- ctx.response.status(500).json({
1771
- error: "Internal Server Error",
1772
- message: "Response validation failed"
2310
+ throw new InternalServerError("Response validation failed", {
2311
+ responseSchema: responseSchema.description || "Unknown schema",
2312
+ validationError: extractZodFieldErrors(error),
2313
+ originalResponse: body
1773
2314
  });
1774
- return ctx.response;
1775
2315
  }
1776
2316
  };
1777
2317
  await next();
@@ -1782,11 +2322,25 @@ function createResponseValidator(responseSchema, debug = false) {
1782
2322
  debug
1783
2323
  };
1784
2324
  }
1785
- function formatValidationError(error) {
1786
- if (error && typeof error === "object" && "format" in error && typeof error.format === "function") {
1787
- return error.format();
2325
+ function extractZodFieldErrors(error) {
2326
+ if (error instanceof z6.ZodError) {
2327
+ const fieldErrorMap = /* @__PURE__ */ new Map();
2328
+ for (const issue of error.issues) {
2329
+ const fieldPath = issue.path.length > 0 ? issue.path.join(".") : "root";
2330
+ if (!fieldErrorMap.has(fieldPath)) {
2331
+ fieldErrorMap.set(fieldPath, []);
2332
+ }
2333
+ fieldErrorMap.get(fieldPath).push(issue.message);
2334
+ }
2335
+ return Array.from(fieldErrorMap.entries()).map(([field, messages]) => ({
2336
+ field,
2337
+ messages
2338
+ }));
1788
2339
  }
1789
- return error instanceof Error ? error.message : String(error);
2340
+ if (error instanceof Error) {
2341
+ return [{ field: "unknown", messages: [error.message] }];
2342
+ }
2343
+ return [{ field: "unknown", messages: [String(error)] }];
1790
2344
  }
1791
2345
 
1792
2346
  // src/router/handlers/executor.ts
@@ -2221,8 +2775,7 @@ function createRouter(options) {
2221
2775
  const match = matcher.match(path6, method);
2222
2776
  if (!match) {
2223
2777
  console.log(`\u274C No match found for: ${method} ${path6}`);
2224
- ctx.response.status(404).json({ error: "Not Found" });
2225
- return;
2778
+ throw new NotFoundError("Not found");
2226
2779
  }
2227
2780
  console.log(`\u2705 Route matched: ${method} ${path6}`);
2228
2781
  console.log(` Params: ${JSON.stringify(match.params)}`);
@@ -2237,14 +2790,7 @@ function createRouter(options) {
2237
2790
  return;
2238
2791
  }
2239
2792
  ctx.request.params = match.params;
2240
- try {
2241
- await executeHandler(ctx, match.route, match.params);
2242
- } catch (error) {
2243
- handleRouteError(ctx, error, {
2244
- detailed: process.env.NODE_ENV !== "production",
2245
- log: true
2246
- });
2247
- }
2793
+ await executeHandler(ctx, match.route, match.params);
2248
2794
  },
2249
2795
  /**
2250
2796
  * Get all registered routes (using optimized registry)
@@ -2305,7 +2851,7 @@ function createRouter(options) {
2305
2851
  }
2306
2852
 
2307
2853
  // src/server/create.ts
2308
- var DEFAULT_OPTIONS = {
2854
+ var DEFAULT_OPTIONS2 = {
2309
2855
  port: 3e3,
2310
2856
  host: "localhost",
2311
2857
  routesDir: "./routes",
@@ -2316,7 +2862,7 @@ var DEFAULT_OPTIONS = {
2316
2862
  plugins: []
2317
2863
  };
2318
2864
  function createServerOptions(options = {}) {
2319
- const baseOptions = { ...DEFAULT_OPTIONS };
2865
+ const baseOptions = { ...DEFAULT_OPTIONS2 };
2320
2866
  setRuntimeConfig({ routesDir: options.routesDir || baseOptions.routesDir });
2321
2867
  return {
2322
2868
  port: options.port ?? baseOptions.port,
@@ -2436,6 +2982,90 @@ function create3(options = {}) {
2436
2982
  return serverInstance;
2437
2983
  }
2438
2984
 
2985
+ // src/errors/unauthorized-error.ts
2986
+ var UnauthorizedError = class extends BlaizeError {
2987
+ /**
2988
+ * Creates a new UnauthorizedError instance
2989
+ *
2990
+ * @param title - Human-readable error message
2991
+ * @param details - Optional authentication context
2992
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
2993
+ */
2994
+ constructor(title, details = void 0, correlationId = void 0) {
2995
+ super(
2996
+ "UNAUTHORIZED" /* UNAUTHORIZED */,
2997
+ title,
2998
+ 401,
2999
+ // HTTP 401 Unauthorized
3000
+ correlationId ?? getCurrentCorrelationId(),
3001
+ details
3002
+ );
3003
+ }
3004
+ };
3005
+
3006
+ // src/errors/forbidden-error.ts
3007
+ var ForbiddenError = class extends BlaizeError {
3008
+ /**
3009
+ * Creates a new ForbiddenError instance
3010
+ *
3011
+ * @param title - Human-readable error message
3012
+ * @param details - Optional permission context
3013
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3014
+ */
3015
+ constructor(title, details = void 0, correlationId = void 0) {
3016
+ super(
3017
+ "FORBIDDEN" /* FORBIDDEN */,
3018
+ title,
3019
+ 403,
3020
+ // HTTP 403 Forbidden
3021
+ correlationId ?? getCurrentCorrelationId(),
3022
+ details
3023
+ );
3024
+ }
3025
+ };
3026
+
3027
+ // src/errors/conflict-error.ts
3028
+ var ConflictError = class extends BlaizeError {
3029
+ /**
3030
+ * Creates a new ConflictError instance
3031
+ *
3032
+ * @param title - Human-readable error message
3033
+ * @param details - Optional conflict context
3034
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3035
+ */
3036
+ constructor(title, details = void 0, correlationId = void 0) {
3037
+ super(
3038
+ "CONFLICT" /* CONFLICT */,
3039
+ title,
3040
+ 409,
3041
+ // HTTP 409 Conflict
3042
+ correlationId ?? getCurrentCorrelationId(),
3043
+ details
3044
+ );
3045
+ }
3046
+ };
3047
+
3048
+ // src/errors/rate-limit-error.ts
3049
+ var RateLimitError = class extends BlaizeError {
3050
+ /**
3051
+ * Creates a new RateLimitError instance
3052
+ *
3053
+ * @param title - Human-readable error message
3054
+ * @param details - Optional rate limit context
3055
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3056
+ */
3057
+ constructor(title, details = void 0, correlationId = void 0) {
3058
+ super(
3059
+ "RATE_LIMITED" /* RATE_LIMITED */,
3060
+ title,
3061
+ 429,
3062
+ // HTTP 429 Too Many Requests
3063
+ correlationId ?? getCurrentCorrelationId(),
3064
+ details
3065
+ );
3066
+ }
3067
+ };
3068
+
2439
3069
  // src/index.ts
2440
3070
  var VERSION = "0.1.0";
2441
3071
  var ServerAPI = { createServer: create3 };
@@ -2466,11 +3096,21 @@ var Blaize = {
2466
3096
  var index_default = Blaize;
2467
3097
  export {
2468
3098
  Blaize,
3099
+ BlaizeError,
3100
+ ConflictError,
3101
+ ErrorSeverity,
3102
+ ErrorType,
3103
+ ForbiddenError,
3104
+ InternalServerError,
2469
3105
  MiddlewareAPI,
3106
+ NotFoundError,
2470
3107
  PluginsAPI,
3108
+ RateLimitError,
2471
3109
  RouterAPI,
2472
3110
  ServerAPI,
3111
+ UnauthorizedError,
2473
3112
  VERSION,
3113
+ ValidationError,
2474
3114
  compose,
2475
3115
  createDeleteRoute,
2476
3116
  createGetRoute,
@@ -2482,6 +3122,7 @@ export {
2482
3122
  createPostRoute,
2483
3123
  createPutRoute,
2484
3124
  create3 as createServer,
2485
- index_default as default
3125
+ index_default as default,
3126
+ isBodyParseError
2486
3127
  };
2487
3128
  //# sourceMappingURL=index.js.map