galbe 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,49 @@
1
1
  # Error handler
2
2
 
3
- Any error happening during a request lifecycle will be intercepted by the error handler.
3
+ Any error happening during a request lifecycle will be intercepted by the error
4
+ handler.
4
5
 
5
- You can customize the default error handling behavior by defining a custom error handler using Galbe's intance `onError` method.
6
+ You can customize the default error handling behavior by defining a custom error
7
+ handler using Galbe's intance `onError` method.
6
8
 
7
9
  ```js
8
- const galbe = new Galbe()
9
- galbe.onError(customErrorHandler)
10
+ const galbe = new Galbe();
11
+ galbe.onError(customErrorHandler);
10
12
  ```
11
13
 
12
14
  ## Definition
13
15
 
14
- The error handler should be a function that takes two aguments: an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) and a [Context](context.md). This function may potentially return a [Response type](handler.md#response-types).
16
+ The error handler should be a function that takes two aguments: an
17
+ [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
18
+ and a [Context](context.md). This function may potentially return a
19
+ [Response type](handler.md#response-types).
15
20
 
16
21
  ```js
17
- galbe.onErrorHandler((error, ctx) => {
22
+ galbe.onError((error, ctx) => {
18
23
  if (error.status === 500) {
19
- return new Response(`Server error ❌`, { status: 500 })
24
+ return new Response(`Server error ❌`, { status: 500 });
20
25
  }
21
26
  if (error.status === 404) {
22
- return new Response(`Not found 🔎`, { status: 404 })
27
+ return new Response(`Not found 🔎`, { status: 404 });
23
28
  }
24
- })
29
+ });
25
30
  ```
26
31
 
27
- The `error` argument could be any type of error thrown by your application. If the error originates from Galbe framework, it will be an instance of [RequestError](#request-error).
32
+ The `error` argument could be any type of error thrown by your application. If
33
+ the error originates from Galbe framework, it will be an instance of
34
+ [RequestError](#request-error).
28
35
 
29
- For instance, the [Router](router.md) will throw a `RequestError` with a `404` status if no route matches the incoming request path. Similarly, the Parser will throw a `RequestError` with a `400` status.
36
+ For instance, the [Router](router.md) will throw a `RequestError` with a `404`
37
+ status if no route matches the incoming request path. Similarly, the Parser will
38
+ throw a `RequestError` with a `400` status.
30
39
 
31
40
  ## Request Error
32
41
 
33
- The `RequestError` class is utilized to instanciate a runtime request error in Galbe. It has two optional attributes: a `status` and a `payload`.
42
+ The `RequestError` class is utilized to instanciate a runtime request error in
43
+ Galbe. It has two optional attributes: a `status` and a `payload`.
34
44
 
35
- If your application throws a `RequestError` instance, Galbe will, by default, construct a Response from your `RequestError` and send it back to the client.
45
+ If your application throws a `RequestError` instance, Galbe will, by default,
46
+ construct a Response from your `RequestError` and send it back to the client.
36
47
 
37
48
  ```js
38
49
  import { Galbe, RequestError } from 'galbe'
@@ -1,12 +1,16 @@
1
1
  # Getting started
2
2
 
3
- Galbe is a Javascript web framework for building fast and versatile backend servers with Bun.
3
+ Galbe is a Javascript web framework for building fast and versatile backend
4
+ servers with Bun.
4
5
 
5
- Designed with simplicity in mind, Galbe allows you to quickly create and set up a project. In addition to its ease of use, Galbe also offers a range of useful features that help you focus on the core logic of your application.
6
+ Designed with simplicity in mind, Galbe allows you to quickly create and set up
7
+ a project. In addition to its ease of use, Galbe also offers a range of useful
8
+ features that help you focus on the core logic of your application.
6
9
 
7
10
  ## Requirements
8
11
 
9
- To start developing your Galbe project, you first need to install [Bun](https://bun.sh).
12
+ To start developing your Galbe project, you first need to install
13
+ [Bun](https://bun.sh).
10
14
 
11
15
  ## Automatic installation
12
16
 
@@ -16,7 +20,9 @@ This is the recommended way of setting up a Galbe project.
16
20
  $ bun create galbe app
17
21
  ```
18
22
 
19
- The Galbe starter CLI will request you to chose a template and a target language for your project. Let's select `hello` as template and `ts` as language. This will create a new project under `app` directory.
23
+ The Galbe starter CLI will request you to chose a template and a target language
24
+ for your project. Let's select `hello` as template and `ts` as language. This
25
+ will create a new project under `app` directory.
20
26
 
21
27
  Now you can navigate to your newly created project and install it:
22
28
 
@@ -46,8 +52,9 @@ $ curl localhost:3000/hello/John?age=32
46
52
  Hello John! You're 32 y.o.
47
53
  ```
48
54
 
49
- > [!TIP]
50
- > If you want to have a more complete view of Galbe capabilities, feel free to take a look at the `demo` template from the Galbe starter CLI.
55
+ > [!TIP]
56
+ > If you want to have a more complete view of Galbe capabilities, feel free to
57
+ > take a look at the `demo` template from the Galbe starter CLI.
51
58
 
52
59
  ## Manual installation
53
60
 
@@ -70,30 +77,37 @@ Open `package.json` file and add the following scripts:
70
77
  }
71
78
  ```
72
79
 
73
- As you can see, those scripts rely on Galbe CLI to run and build the application. You will find more info about it on the [CLI](cli.md) page.
80
+ As you can see, those scripts rely on Galbe CLI to run and build the
81
+ application. You will find more info about it on the [CLI](cli.md) page.
74
82
 
75
- This require your `index.ts` to export a default Galbe instance in order to work. As in the following example:
83
+ This require your `index.ts` to export a default Galbe instance in order to
84
+ work. As in the following example:
76
85
 
77
86
  ```ts
78
- import { Galbe } from 'galbe'
87
+ import { Galbe } from "galbe";
79
88
 
80
- const galbe = new Galbe({ port: 3000 })
81
- galbe.get('/hello', () => 'Hello Mom!')
89
+ const galbe = new Galbe({ port: 3000 });
90
+ galbe.get("/hello", () => "Hello Mom!");
82
91
 
83
- export default galbe
92
+ export default galbe;
84
93
  ```
85
94
 
86
- This is the recommended way to proceed but it is not mandatory. Galbe instances also provide a `listen` method that will allow you to manually start your server instance from the code.
95
+ This is the recommended way to proceed but it is not mandatory. Galbe instances
96
+ also provide a `listen` method that will allow you to manually start your server
97
+ instance from the code.
87
98
 
88
99
  > [!WARNING]
89
- > In the case you decide to not rely on Galbe CLI to run/build your app, you will not have access to [Automatic Route Analyzer](routes.md#automatic-route-analyzer) feature.
100
+ > In the case you decide to not rely on Galbe CLI to run/build your app, you
101
+ > will not have access to
102
+ > [Automatic Route Analyzer](routes.md#automatic-route-analyzer) feature.
90
103
 
91
104
  ## Configuration
92
105
 
93
- To configure your Galbe server, you should pass your configuration to the Galbe constructor when you instanciate it.
106
+ To configure your Galbe server, you should pass your configuration to the Galbe
107
+ constructor when you instanciate it.
94
108
 
95
109
  ```ts
96
- const galbe = new Galbe(configuration)
110
+ const galbe = new Galbe(configuration);
97
111
  ```
98
112
 
99
113
  ### Properties
@@ -112,11 +126,14 @@ The base path is added as a prefix to all the routes created.
112
126
 
113
127
  **routes**
114
128
 
115
- A Glob Pattern or a list of Glob patterns defining the route files to be analyzed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer). Default is `src/**/*.route.{js,ts}`.
129
+ A Glob Pattern or a list of Glob patterns defining the route files to be
130
+ analyzed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer).
131
+ Default is `src/**/*.route.{js,ts}`.
116
132
 
117
133
  **plugin**
118
134
 
119
- A property that can be used by plugins to add plugin's specific configuration. Every key should correspond to a [Unique Plugin Identifier](plugins.md).
135
+ A property that can be used by plugins to add plugin's specific configuration.
136
+ Every key should correspond to a [Unique Plugin Identifier](plugins.md).
120
137
 
121
138
  **tls**
122
139
 
@@ -130,15 +147,21 @@ Enable or disable TLS support. Default value is `false`.
130
147
 
131
148
  **requestValidator.enabled**
132
149
 
133
- Enable or disable the _request_ schema validation (See [Request Schema definition](schemas.md#request-schema-definition)). Default value is `true`.
150
+ Enable or disable the _request_ schema validation (See
151
+ [Request Schema definition](schemas.md#request-schema-definition)). Default
152
+ value is `true`.
134
153
 
135
154
  **responseValidator.enabled**
136
155
 
137
- Enable or disable the _response_ schema validation (See [Request Schema definition](schemas.md#request-schema-definition)). Default value is `true`.
156
+ Enable or disable the _response_ schema validation (See
157
+ [Request Schema definition](schemas.md#request-schema-definition)). Default
158
+ value is `true`.
138
159
 
139
160
  ### Examples
140
161
 
141
- A common way to handle server configuration is to create new file `galbe.config.(js|ts|json)` at the root of your project directory and import it in your code. Here is an example:
162
+ A common way to handle server configuration is to create new file
163
+ `galbe.config.(js|ts|json)` at the root of your project directory and import it
164
+ in your code. Here is an example:
142
165
 
143
166
  galbe.config.js
144
167
 
@@ -152,27 +175,31 @@ export default {
152
175
  index.js
153
176
 
154
177
  ```js
155
- import { Galbe } from 'galbe'
156
- import config from './galbe.config'
178
+ import { Galbe } from "galbe";
179
+ import config from "./galbe.config";
157
180
 
158
- export default new Galbe(config)
181
+ export default new Galbe(config);
159
182
  ```
160
183
 
161
- > [!TIP]
162
- > If you are using Typescript, you can import `GalbeConfig` type from galbe package to ensure type consistency for your configuration. Here is an example:
184
+ > [!TIP]
185
+ > If you are using Typescript, you can import `GalbeConfig` type from galbe
186
+ > package to ensure type consistency for your configuration. Here is an example:
163
187
  >
164
188
  > ```ts
165
- > import type { GalbeConfig } from 'galbe'
189
+ > import type { GalbeConfig } from "galbe";
166
190
  > const config: GalbeConfig = {
167
191
  > port: Number(Bun.env.GALBE_PORT),
168
- > routes: 'routes/*.route.ts'
169
- > }
170
- > export default config
192
+ > routes: "routes/*.route.ts",
193
+ > };
194
+ > export default config;
171
195
  > ```
172
196
 
173
197
  ## Project structure
174
198
 
175
- One key aspect of Galbe, is its versatility in terms of project structure. This is partly allowed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer) and the `routes` config property which defaults to `src/**/*.route.{js,ts}`.
199
+ One key aspect of Galbe, is its versatility in terms of project structure. This
200
+ is partly allowed by the
201
+ [Automatic Route Analyzer](routes.md#automatic-route-analyzer) and the `routes`
202
+ config property which defaults to `src/**/*.route.{js,ts}`.
176
203
 
177
204
  Here are two examples of valid project structures by default:
178
205
 
@@ -214,18 +241,26 @@ Here are two examples of valid project structures by default:
214
241
  └── tsconfig.json
215
242
  ```
216
243
 
217
- In both cases, the [Automatic Route Analyzer](routes.md#automatic-route-analyzer) will analyze `foo.route.ts` and `bar.route.ts` Route Files to find route definitions.
244
+ In both cases, the
245
+ [Automatic Route Analyzer](routes.md#automatic-route-analyzer) will analyze
246
+ `foo.route.ts` and `bar.route.ts` Route Files to find route definitions.
218
247
 
219
- You can find more info about Route Files definition in the [Routes Files](routes.md#route-files) section.
248
+ You can find more info about Route Files definition in the
249
+ [Routes Files](routes.md#route-files) section.
220
250
 
221
251
  > [!NOTE]
222
- > The examples provided above will work with the default configuration, but you can easily customize the routes property to fit your own project structure. Simply redefine the `routes` property with your own pattern(s) to to fit your own project structure.
252
+ > The examples provided above will work with the default configuration, but you
253
+ > can easily customize the routes property to fit your own project structure.
254
+ > Simply redefine the `routes` property with your own pattern(s) to to fit your
255
+ > own project structure.
223
256
 
224
257
  ## How to debug
225
258
 
226
- The easiest way to debug your app is by installing the [VSCode Bun extension](https://marketplace.visualstudio.com/items?itemName=oven.bun-vscode).
259
+ The easiest way to debug your app is by installing the
260
+ [VSCode Bun extension](https://marketplace.visualstudio.com/items?itemName=oven.bun-vscode).
227
261
 
228
- You can then create a `.vscode/launch.json` config file in your project root directory. Here is an example of configuration:
262
+ You can then create a `.vscode/launch.json` config file in your project root
263
+ directory. Here is an example of configuration:
229
264
 
230
265
  ```json
231
266
  {
@@ -239,7 +274,7 @@ You can then create a `.vscode/launch.json` config file in your project root dir
239
274
  "env": { "TERM": "xterm" },
240
275
  "cwd": "${workspaceFolder}",
241
276
  "runtime": "bun",
242
- "runtimeArgs": ["dev", "index.ts", "--watch", "--force"]
277
+ "runtimeArgs": ["dev", "index.ts", "-w", "."]
243
278
  }
244
279
  ]
245
280
  }
package/docs/routes.md CHANGED
@@ -111,3 +111,6 @@ export default g => {
111
111
  g.get('/foo/:bar', ctx => ctx.params.bar)
112
112
  }
113
113
  ```
114
+
115
+ > [!TIP]
116
+ > You can ignore a specific route from being analyzed by adding a `//@galbe-ignore` comment before the route definition. This is useful if you want to exclude certain routes from automatic analysis or documentation generation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  "license": "MIT",
30
30
  "scripts": {
31
31
  "test": "bun test",
32
+ "typecheck": "tsc --noEmit --emitDeclarationOnly false",
32
33
  "postinstall": "bun run ./scripts/postinstall.ts",
33
34
  "release": "release-it"
34
35
  },
@@ -12,10 +12,10 @@ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
12
12
  isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
13
13
  ? 'application/json'
14
14
  : format === 'byte'
15
- ? 'application/octet-stream'
16
- : type === 'string'
17
- ? 'text/plain'
18
- : '*/*'
15
+ ? 'application/octet-stream'
16
+ : type === 'string'
17
+ ? 'text/plain'
18
+ : '*/*'
19
19
 
20
20
  export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
21
21
  let paths: any = {}
@@ -115,9 +115,9 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
115
115
  type: type,
116
116
  ...(type === 'object'
117
117
  ? {
118
- properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
119
- ...(required.length ? { required } : {})
120
- }
118
+ properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
119
+ ...(required.length ? { required } : {})
120
+ }
121
121
  : {})
122
122
  }
123
123
  } else if (kind === 'union') {
@@ -178,8 +178,13 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
178
178
  (routes, c) => ({ ...routes, ...c.routes }),
179
179
  {} as Record<string, Record<string, Record<string, any>>>
180
180
  )
181
+ let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter((([_, d]) => d?.static)))
182
+
181
183
  walkRoutes(g.router.routes, r => {
182
184
  let meta = metaRoutes?.[r.path]?.[r.method]
185
+ if (r.static?.root)
186
+ meta = metaStatic[r.static?.root]?.static
187
+ if (meta?.hide) return
183
188
  let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
184
189
  if (!(path in paths)) paths[path] = {}
185
190
  let tags = [
@@ -196,19 +201,19 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
196
201
  : []
197
202
  let headerParam = r.schema?.headers
198
203
  ? Object.entries(r.schema?.headers as Record<string, STSchema>)
199
- .map(([k, v]) => {
200
- let p = parseParam(k, v, 'header')
201
- if (k.match(/authorization/i)) {
202
- // TODO: handle other auth methods
203
- if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
204
- security.push({ bearerAuth: [] })
205
- components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
206
- return null
207
- }
204
+ .map(([k, v]) => {
205
+ let p = parseParam(k, v, 'header')
206
+ if (k.match(/authorization/i)) {
207
+ // TODO: handle other auth methods
208
+ if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
209
+ security.push({ bearerAuth: [] })
210
+ components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
211
+ return null
208
212
  }
209
- return p
210
- })
211
- .filter(p => p)
213
+ }
214
+ return p
215
+ })
216
+ .filter(p => p)
212
217
  : []
213
218
  // TODO cookieParam
214
219
  let parameters = [...pathParam, ...queryParam, ...headerParam]
@@ -234,6 +239,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
234
239
  if (r.schema.response && Object.keys(r.schema.response).length) {
235
240
  responses = Object.fromEntries(
236
241
  Object.entries(r.schema.response).map(([status, v]) => {
242
+ if(!v) return []
237
243
  let s = Number(status) as keyof typeof HttpStatus
238
244
  let { schema, isJson } = schemaToOpenapi(v)
239
245
  let { type, format } = resolveRef(schema)
@@ -242,8 +248,8 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
242
248
  description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
243
249
  content: { [media]: { schema: schema } }
244
250
  }
245
- if (components.responses && r.schema.response?.[s].id) {
246
- components.responses[r.schema.response?.[s].id as string] = response
251
+ if (components.responses && r.schema.response?.[s]?.id) {
252
+ components.responses[r.schema.response?.[s]?.id as string] = response
247
253
  //@ts-ignore
248
254
  response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
249
255
  }
package/src/index.ts CHANGED
@@ -14,9 +14,14 @@ import type {
14
14
  STResponse,
15
15
  STParams,
16
16
  STHeaders,
17
- STQuery
17
+ STQuery,
18
+ StaticEndpoint,
19
+ Route,
20
+ StaticEndpointOptions
18
21
  } from './types'
19
22
 
23
+ import { readdirSync, statSync } from 'fs'
24
+ import { resolve as resolvePath } from 'path'
20
25
  import server from './server'
21
26
  import { GalbeRouter } from './router'
22
27
  import { SchemaType, type STObject, type Static } from './schema'
@@ -41,7 +46,7 @@ const overloadDiscriminer = <
41
46
  | Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
42
47
  | Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
43
48
  arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
44
- ) => {
49
+ ): Route<M, Path, P, H, Q, B, R> => {
45
50
  const defaultSchema = {}
46
51
  if (typeof arg2 === 'function') {
47
52
  return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
@@ -70,7 +75,7 @@ const galbeMethod = <
70
75
  schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
71
76
  hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
72
77
  handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
73
- ) => {
78
+ ): Route<M, Path, P, H, Q, B, R> => {
74
79
  schema = schema ?? {}
75
80
  hooks = hooks || []
76
81
  const context: Context<M, Path, typeof schema> = {
@@ -85,7 +90,8 @@ const galbeMethod = <
85
90
  state: {},
86
91
  set: {} as {
87
92
  headers: {
88
- [header: string]: string
93
+ 'set-cookie': string[]
94
+ [header: string]: string | string[]
89
95
  }
90
96
  status?: number
91
97
  }
@@ -306,6 +312,40 @@ export class Galbe {
306
312
  | Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
307
313
  arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
308
314
  ) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
315
+ static: StaticEndpoint = (path: string, target: string, options?: StaticEndpointOptions) => {
316
+ let { resolve } = options ?? {}
317
+ const rootPath = path
318
+
319
+ const walkStatic = (path: string, target: string) => {
320
+ path = path?.[0] === '/' ? path : `/${path}`
321
+ path = path.endsWith('/') ? path.slice(0, -1) : path;
322
+
323
+ let t = target
324
+ if (Bun.env.BUN_ENV === 'production') {
325
+ t = resolvePath(import.meta.dir, `static-${Bun.env.GALBE_BUILD}/${target}`)
326
+ }
327
+
328
+ if (!statSync(t).isDirectory()) {
329
+ let ut: string | null | undefined | void = t
330
+ if (path.endsWith('.html')) path = path.slice(0, -5)
331
+ if (resolve) ut = resolve(path, ut)
332
+ if (ut) {
333
+ let handler = () => new Response(Bun.file(ut))
334
+ this.add({ ...galbeMethod(this, 'get', path, {}, undefined, handler), static: { path: ut, root: rootPath } })
335
+ }
336
+ } else {
337
+ let root = readdirSync(t)
338
+ for (let f of root) {
339
+ let p = f === 'index.html' ? path : `${path}/${f}`
340
+ walkStatic(p, `${target}/${f}`)
341
+ }
342
+ }
343
+
344
+ return { ...galbeMethod(this, 'get', path, {}, undefined, () => { }), static: { path: t, root: rootPath } }
345
+ }
346
+
347
+ return walkStatic(path, target)
348
+ }
309
349
  }
310
350
 
311
351
  export * from './types'
package/src/parser.ts CHANGED
@@ -89,8 +89,8 @@ export const requestBodyParser = async (
89
89
  let received = contentType?.match(FORM_HEADER_RX)
90
90
  ? 'urlForm'
91
91
  : contentType?.match(MP_HEADER_RX)
92
- ? 'multipartForm'
93
- : contentType
92
+ ? 'multipartForm'
93
+ : contentType
94
94
  throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received ${received}` } })
95
95
  } else if (!contentType || contentType === BA_HEADER) {
96
96
  if (!schema) {
@@ -377,7 +377,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
377
377
  payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
378
378
  })
379
379
  }
380
- const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
380
+ const parseMultipartHeader = (header: string): { name: string;[key: string]: string } | null => {
381
381
  if (!header) return null
382
382
  let disposition = 'form-data'
383
383
  const multipartHeader = [
@@ -537,24 +537,24 @@ const paramParser = (
537
537
  if (typeof value === 'boolean') return value
538
538
  if (value === 'true') return true
539
539
  if (value === 'false') return false
540
- else throw `${value} is not a valid boolean. Should be 'true' or 'false'`
540
+ else throw `Not a valid boolean. Should be 'true' or 'false'`
541
541
  } else if (type[Kind] === 'integer') {
542
- if (value === null || value === undefined) throw `${value} is not a valid integer`
542
+ if (value === null || value === undefined) throw `Not a valid integer`
543
543
  const parsedValue = parseInt(value, 10)
544
- if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid integer`
544
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid integer`
545
545
  validate(parsedValue, type)
546
546
  return parsedValue
547
547
  } else if (type[Kind] === 'number') {
548
- if (value === null || value === undefined) throw `${value} is not a valid number`
548
+ if (value === null || value === undefined) throw `Not a valid number`
549
549
  const parsedValue = Number(value)
550
- if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid number`
550
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid number`
551
551
  validate(parsedValue, type)
552
552
  return parsedValue
553
553
  } else if (type[Kind] === 'string') {
554
554
  validate(value, type)
555
555
  return value
556
556
  } else if (type[Kind] === 'literal') {
557
- if (value !== type.value) throw `${value} is not a valid value`
557
+ if (value !== type.value) throw `Not a valid value`
558
558
  return value
559
559
  } else if (type[Kind] === 'array') {
560
560
  return [paramParser(value, type.items as STMultipartFormValues) as Static<STUrlFormValues>]
@@ -569,9 +569,9 @@ const paramParser = (
569
569
  continue
570
570
  }
571
571
  }
572
- throw `${value} could not be parsed to any of ${union
572
+ throw `Could not be parsed to any of [${union
573
573
  .map(u => (u as STLiteral)?.value ?? (u as STSchema)[Kind])
574
- .join(', ')}`
574
+ .join(', ')}]`
575
575
  } else if (type[Kind] === 'any') {
576
576
  return value
577
577
  }
@@ -645,12 +645,17 @@ export const parseEntry = <T extends STProps>(
645
645
  export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
646
646
  const details = {
647
647
  status: ctx.set.status || 200,
648
- headers: new Headers(ctx.set.headers)
648
+ headers: new Headers()
649
+ }
650
+ for (const [key, value] of Object.entries(ctx.set.headers)) {
651
+ if (Array.isArray(value)) {
652
+ value.forEach(v => details.headers.append(key, v))
653
+ } else details.headers.set(key, value)
649
654
  }
650
655
  if (response instanceof Response) return response
651
656
  else if (typeof response === 'string') {
652
657
  if (!details?.headers?.has('content-type')) {
653
- if (schema?.[details.status][Kind] === 'json') {
658
+ if (schema?.[details.status]?.[Kind] === 'json') {
654
659
  details?.headers?.set('content-type', 'application/json')
655
660
  response = `"${response}"`
656
661
  } else details?.headers?.set('content-type', 'text/plain')