mockaton 10.0.0 → 10.2.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/README.md CHANGED
@@ -15,7 +15,7 @@ An HTTP mock server for simulating APIs with minimal setup
15
15
  ## Motivation
16
16
 
17
17
  **No API state should be too hard to test.**
18
- With Mockaton, developers can achieve correctness and increase speed.
18
+ With Mockaton, developers can achieve correctness and speed.
19
19
 
20
20
  ### Correctness
21
21
  - Enables testing of complex scenarios that would otherwise be skipped. e.g.,
@@ -60,9 +60,9 @@ Nonetheless, there’s a programmatic API, which is handy
60
60
  for setting up tests (see **Commander API** section below).
61
61
 
62
62
  <picture>
63
- <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.vp810x768.light.gold.png">
64
- <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.vp810x768.dark.gold.png">
65
- <img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.vp810x768.light.gold.png">
63
+ <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.vp781x772.light.gold.png">
64
+ <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.vp781x772.dark.gold.png">
65
+ <img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.vp781x772.light.gold.png">
66
66
  </picture>
67
67
 
68
68
 
@@ -174,7 +174,7 @@ Since Mockaton has no dependencies, you can create an executable
174
174
  by linking to `src/cli.js`.
175
175
 
176
176
  ```shell
177
- git clone https://github.com/ericfortis/mockaton.git
177
+ git clone https://github.com/ericfortis/mockaton.git --depth 1
178
178
  ln -s `realpath mockaton/src/cli.js` ~/bin/mockaton # some dir in your $PATH
179
179
  ```
180
180
 
@@ -516,7 +516,7 @@ const server = await Mockaton(
516
516
  ## Demo App (Vite + React)
517
517
 
518
518
  ```sh
519
- git clone https://github.com/ericfortis/mockaton.git
519
+ git clone https://github.com/ericfortis/mockaton.git --depth 1
520
520
  cd mockaton/demo-app-vite
521
521
  npm install
522
522
 
package/index.d.ts CHANGED
@@ -45,7 +45,7 @@ interface Config {
45
45
  }
46
46
 
47
47
 
48
- export function Mockaton(options: Partial<Config>): Server | undefined
48
+ export function Mockaton(options: Partial<Config>): Promise<Server | undefined>
49
49
  export function defineConfig(options: Partial<Config>): Partial<Config>
50
50
 
51
51
  export const jsToJsonPlugin: Plugin
@@ -97,5 +97,5 @@ export interface State {
97
97
  collectProxied: boolean
98
98
  proxyFallback: string
99
99
 
100
- corsAllowed: boolean
100
+ corsAllowed?: boolean
101
101
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.0.0",
5
+ "version": "10.2.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -104,7 +104,7 @@ function reinitialize(_, response) {
104
104
  async function selectCookie(req, response) {
105
105
  const error = cookie.setCurrent(await parseJSON(req))
106
106
  if (error)
107
- sendUnprocessableContent(response, error)
107
+ sendUnprocessableContent(response, error?.message || error)
108
108
  else
109
109
  sendOK(response)
110
110
  }
package/src/Dashboard.css CHANGED
@@ -157,7 +157,7 @@ header {
157
157
  align-items: flex-end;
158
158
  gap: 16px 10px;
159
159
 
160
- @media (max-width: 800px) {
160
+ @media (max-width: 780px) {
161
161
  max-width: 400px;
162
162
  }
163
163
  }
@@ -315,7 +315,7 @@ main {
315
315
  min-width: 0;
316
316
  min-height: 0;
317
317
 
318
- @media (max-width: 800px) {
318
+ @media (max-width: 780px) {
319
319
  flex-direction: column;
320
320
 
321
321
  .Resizer {
@@ -335,6 +335,7 @@ main {
335
335
  padding: 16px;
336
336
  padding-bottom: 0;
337
337
  border-right: 1px solid var(--colorSecondaryActionBorder);
338
+ user-select: none;
338
339
  overflow-y: auto;
339
340
  box-shadow: var(--boxShadow1);
340
341
  }
@@ -437,7 +438,11 @@ table {
437
438
  display: flex;
438
439
 
439
440
  > input {
440
- appearance: none;
441
+ /* For click drag target */
442
+ position: absolute;
443
+ width: 22px;
444
+ height: 22px;
445
+ opacity: 0;
441
446
 
442
447
  &:focus {
443
448
  outline: 0;
@@ -5,7 +5,7 @@
5
5
  <link rel="stylesheet" href="./mockaton/Dashboard.css">
6
6
  <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <meta name="description" content="Mock Server for developing UIs">
8
+ <meta name="description" content="HTTP Mock Server">
9
9
  <title>Mockaton</title>
10
10
  </head>
11
11
  <body>
package/src/Dashboard.js CHANGED
@@ -78,8 +78,10 @@ for (const k of Object.keys(CSS))
78
78
 
79
79
 
80
80
  /** @type {State & {
81
- * groupByMethod: boolean,
82
81
  * canProxy: boolean
82
+ * groupByMethod: boolean
83
+ * toggleGroupByMethod: () => void
84
+ * leftSideWidth?: number
83
85
  * }} */
84
86
  const state = {
85
87
  brokersByMethod: {},
@@ -87,6 +89,7 @@ const state = {
87
89
  cookies: [],
88
90
  comments: [],
89
91
  delay: 0,
92
+
90
93
  collectProxied: false,
91
94
  proxyFallback: '',
92
95
  get canProxy() {
@@ -98,8 +101,8 @@ const state = {
98
101
  this.groupByMethod = !this.groupByMethod
99
102
  localStorage.setItem('groupByMethod', String(this.groupByMethod))
100
103
  },
101
-
102
- leftSideWidth: undefined,
104
+
105
+ leftSideWidth: undefined
103
106
  }
104
107
 
105
108
  const mockaton = new Commander(window.location.origin)
@@ -127,19 +130,19 @@ const leftSideRef = useRef()
127
130
  function App() {
128
131
  const { leftSideWidth } = state
129
132
  return [
130
- r(Header),
131
- r(Menu),
133
+ Header(),
134
+ Menu(),
132
135
  r('main', null,
133
136
  r('div', {
134
137
  ref: leftSideRef,
135
138
  style: { width: leftSideWidth + 'px' },
136
139
  className: CSS.leftSide
137
140
  },
138
- r(MockList),
139
- r(StaticFilesList)),
141
+ MockList(),
142
+ StaticFilesList()),
140
143
  r('div', { className: CSS.rightSide },
141
- r(Resizer),
142
- r(PayloadViewer)))
144
+ Resizer(),
145
+ PayloadViewer()))
143
146
  ]
144
147
  }
145
148
 
@@ -153,15 +156,15 @@ function Header() {
153
156
  width: 160
154
157
  }),
155
158
  r('div', null,
156
- r(GlobalDelayField),
157
- r(CookieSelector),
158
- r(BulkSelector),
159
- r(ProxyFallbackField),
160
- r(ResetButton)),
159
+ GlobalDelayField(),
160
+ CookieSelector(),
161
+ BulkSelector(),
162
+ ProxyFallbackField(),
163
+ ResetButton()),
161
164
  r('button', {
162
165
  className: CSS.MenuTrigger,
163
166
  popovertarget: 'Menu'
164
- }, r(SettingsIcon))
167
+ }, SettingsIcon())
165
168
  ))
166
169
  }
167
170
 
@@ -251,7 +254,7 @@ function GlobalDelayField() {
251
254
  }
252
255
  return (
253
256
  r('label', className(CSS.Field, CSS.GlobalDelayField),
254
- r('span', null, r(TimerIcon), Strings.delay_ms),
257
+ r('span', null, TimerIcon(), Strings.delay_ms),
255
258
  r('input', {
256
259
  type: 'number',
257
260
  min: 0,
@@ -263,7 +266,7 @@ function GlobalDelayField() {
263
266
  }
264
267
 
265
268
  function ProxyFallbackField() {
266
- const { proxyFallback, collectProxied } = state
269
+ const { proxyFallback } = state
267
270
  function onChange() {
268
271
  const saveCheckbox = this.closest(`.${CSS.FallbackBackend}`).querySelector('[type=checkbox]')
269
272
  saveCheckbox.disabled = !this.validity.valid || !this.value.trim()
@@ -279,7 +282,7 @@ function ProxyFallbackField() {
279
282
  return (
280
283
  r('div', className(CSS.Field, CSS.FallbackBackend),
281
284
  r('label', null,
282
- r('span', null, r(CloudIcon), Strings.fallback_server),
285
+ r('span', null, CloudIcon(), Strings.fallback_server),
283
286
  r('input', {
284
287
  type: 'url',
285
288
  autocomplete: 'none',
@@ -287,14 +290,11 @@ function ProxyFallbackField() {
287
290
  value: proxyFallback,
288
291
  onChange
289
292
  })),
290
- r(SaveProxiedCheckbox, {
291
- collectProxied,
292
- disabled: !proxyFallback
293
- })))
293
+ SaveProxiedCheckbox()))
294
294
  }
295
295
 
296
- function SaveProxiedCheckbox({ disabled }) {
297
- const { collectProxied } = state
296
+ function SaveProxiedCheckbox() {
297
+ const { collectProxied, canProxy } = state
298
298
  function onChange() {
299
299
  mockaton.setCollectProxied(this.checked)
300
300
  .then(parseError)
@@ -304,7 +304,7 @@ function SaveProxiedCheckbox({ disabled }) {
304
304
  r('label', className(CSS.SaveProxiedCheckbox),
305
305
  r('input', {
306
306
  type: 'checkbox',
307
- disabled,
307
+ disabled: !canProxy,
308
308
  checked: collectProxied,
309
309
  onChange
310
310
  }),
@@ -330,8 +330,7 @@ function ResetButton() {
330
330
  /** # MockList */
331
331
 
332
332
  function MockList() {
333
- const { brokersByMethod, groupByMethod } = state
334
- const canProxy = state.canProxy
333
+ const { brokersByMethod, groupByMethod, canProxy } = state
335
334
 
336
335
  if (!Object.keys(brokersByMethod).length)
337
336
  return (
@@ -355,14 +354,14 @@ function MockList() {
355
354
  }
356
355
 
357
356
  function Row({ method, urlMask, urlMaskDittoed, broker }) {
358
- const canProxy = state.canProxy
357
+ const { canProxy } = state
359
358
  return (
360
359
  r('tr', { 'data-method': method, 'data-urlMask': urlMask },
361
- canProxy && r('td', null, r(ProxyToggler, { broker })),
362
- r('td', null, r(DelayRouteToggler, { broker })),
363
- r('td', null, r(InternalServerErrorToggler, { broker })),
364
- r('td', null, r(PreviewLink, { method, urlMask, urlMaskDittoed })),
365
- r('td', null, r(MockSelector, { broker }))))
360
+ canProxy && r('td', null, ProxyToggler(broker)),
361
+ r('td', null, DelayRouteToggler(broker)),
362
+ r('td', null, InternalServerErrorToggler(broker)),
363
+ r('td', null, PreviewLink(method, urlMask, urlMaskDittoed)),
364
+ r('td', null, MockSelector(broker))))
366
365
  }
367
366
 
368
367
  function rowsFor(targetMethod) {
@@ -385,7 +384,7 @@ function rowsFor(targetMethod) {
385
384
  }))
386
385
  }
387
386
 
388
- function PreviewLink({ method, urlMask, urlMaskDittoed }) {
387
+ function PreviewLink(method, urlMask, urlMaskDittoed) {
389
388
  async function onClick(event) {
390
389
  event.preventDefault()
391
390
  try {
@@ -408,8 +407,8 @@ function PreviewLink({ method, urlMask, urlMaskDittoed }) {
408
407
  : tail))
409
408
  }
410
409
 
411
- /** @param {{ broker: ClientMockBroker }} props */
412
- function MockSelector({ broker }) {
410
+ /** @param {ClientMockBroker} broker */
411
+ function MockSelector(broker) {
413
412
  const { groupByMethod } = state
414
413
 
415
414
  function onChange() {
@@ -457,29 +456,23 @@ function MockSelector({ broker }) {
457
456
  }, nameFor(file))))))
458
457
  }
459
458
 
460
- /** @param {{ broker: ClientMockBroker }} props */
461
- function DelayRouteToggler({ broker }) {
462
- function onChange() {
459
+ /** @param {ClientMockBroker} broker */
460
+ function DelayRouteToggler(broker) {
461
+ function commit(checked) {
463
462
  const { method, urlMask } = parseFilename(broker.mocks[0])
464
- mockaton.setRouteIsDelayed(method, urlMask, this.checked)
463
+ mockaton.setRouteIsDelayed(method, urlMask, checked)
465
464
  .then(parseError)
466
465
  .catch(onError)
467
466
  }
468
- return (
469
- r('label', {
470
- className: CSS.DelayToggler,
471
- title: Strings.delay
472
- },
473
- r('input', {
474
- type: 'checkbox',
475
- checked: broker.currentMock.delayed,
476
- onChange
477
- }),
478
- TimerIcon()))
467
+ return ClickDragToggler({
468
+ checked: broker.currentMock.delayed,
469
+ commit
470
+ })
479
471
  }
480
472
 
481
- /** @param {{ broker: ClientMockBroker }} props */
482
- function InternalServerErrorToggler({ broker }) {
473
+
474
+ /** @param {ClientMockBroker} broker */
475
+ function InternalServerErrorToggler(broker) {
483
476
  function onChange() {
484
477
  const { urlMask, method } = parseFilename(broker.mocks[0])
485
478
  mockaton.select(
@@ -505,8 +498,8 @@ function InternalServerErrorToggler({ broker }) {
505
498
  r('span', null, '500')))
506
499
  }
507
500
 
508
- /** @param {{ broker: ClientMockBroker }} props */
509
- function ProxyToggler({ broker }) {
501
+ /** @param {ClientMockBroker} broker */
502
+ function ProxyToggler(broker) {
510
503
  function onChange() {
511
504
  const { urlMask, method } = parseFilename(broker.mocks[0])
512
505
  mockaton.setRouteIsProxied(method, urlMask, this.checked)
@@ -525,7 +518,7 @@ function ProxyToggler({ broker }) {
525
518
  checked: !broker.currentMock.file,
526
519
  onChange
527
520
  }),
528
- r(CloudIcon)))
521
+ CloudIcon()))
529
522
  }
530
523
 
531
524
 
@@ -533,8 +526,7 @@ function ProxyToggler({ broker }) {
533
526
  /** # StaticFilesList */
534
527
 
535
528
  function StaticFilesList() {
536
- const { staticBrokers } = state
537
- const canProxy = state.canProxy
529
+ const { staticBrokers, canProxy } = state
538
530
  if (!Object.keys(staticBrokers).length)
539
531
  return null
540
532
  const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
@@ -549,35 +541,28 @@ function StaticFilesList() {
549
541
  r('tbody', null,
550
542
  Object.values(staticBrokers).map((broker, i) =>
551
543
  r('tr', null,
552
- canProxy && r('td', null, r(ProxyStaticToggler, {})),
553
- r('td', null, r(DelayStaticRouteToggler, { broker })),
554
- r('td', null, r(NotFoundToggler, { broker })),
544
+ canProxy && r('td', null, ProxyStaticToggler()),
545
+ r('td', null, DelayStaticRouteToggler(broker)),
546
+ r('td', null, NotFoundToggler(broker)),
555
547
  r('td', null, r('a', { href: broker.route, target: '_blank' }, dp[i]))
556
548
  )))))
557
549
  }
558
550
 
559
- /** @param {{ broker: ClientStaticBroker }} props */
560
- function DelayStaticRouteToggler({ broker }) {
561
- function onChange() {
562
- mockaton.setStaticRouteIsDelayed(broker.route, this.checked)
551
+ /** @param {ClientStaticBroker} broker */
552
+ function DelayStaticRouteToggler(broker) {
553
+ function commit(checked) {
554
+ mockaton.setStaticRouteIsDelayed(broker.route, checked)
563
555
  .then(parseError)
564
556
  .catch(onError)
565
557
  }
566
- return (
567
- r('label', {
568
- className: CSS.DelayToggler,
569
- title: Strings.delay
570
- },
571
- r('input', {
572
- type: 'checkbox',
573
- checked: broker.delayed,
574
- onChange
575
- }),
576
- TimerIcon()))
558
+ return ClickDragToggler({
559
+ checked: broker.delayed,
560
+ commit
561
+ })
577
562
  }
578
563
 
579
- /** @param {{ broker: ClientStaticBroker }} props */
580
- function NotFoundToggler({ broker }) {
564
+ /** @param {ClientStaticBroker} broker */
565
+ function NotFoundToggler(broker) {
581
566
  function onChange() {
582
567
  mockaton.setStaticRouteStatus(broker.route, this.checked ? 404 : 200)
583
568
  .then(parseError)
@@ -596,7 +581,7 @@ function NotFoundToggler({ broker }) {
596
581
  r('span', null, '404')))
597
582
  }
598
583
 
599
- function ProxyStaticToggler({}) { // TODO
584
+ function ProxyStaticToggler() { // TODO
600
585
  function onChange() {
601
586
  }
602
587
  return (
@@ -610,10 +595,44 @@ function ProxyStaticToggler({}) { // TODO
610
595
  disabled: true,
611
596
  onChange
612
597
  }),
613
- r(CloudIcon)))
598
+ CloudIcon()))
614
599
  }
615
600
 
616
601
 
602
+ function ClickDragToggler({ checked, commit }) {
603
+ function onPointerEnter(event) {
604
+ if (event.buttons === 1)
605
+ onPointerDown.call(this)
606
+ }
607
+ function onPointerDown() {
608
+ this.checked = !this.checked
609
+ commit(this.checked)
610
+ }
611
+ function onClick(event) {
612
+ if (event.pointerType === 'mouse')
613
+ event.preventDefault()
614
+ }
615
+ function onChange() {
616
+ commit(this.checked)
617
+ }
618
+ return (
619
+ r('label', {
620
+ className: CSS.DelayToggler,
621
+ title: Strings.delay
622
+ },
623
+ r('input', {
624
+ type: 'checkbox',
625
+ checked,
626
+ onPointerEnter,
627
+ onPointerDown,
628
+ onClick,
629
+ onChange
630
+ }),
631
+ TimerIcon()))
632
+ }
633
+
634
+
635
+
617
636
  function Resizer() {
618
637
  return (
619
638
  r('div', {
@@ -627,8 +646,8 @@ Resizer.panelWidth = 0
627
646
  Resizer.onPointerDown = function (event) {
628
647
  Resizer.initialX = event.clientX
629
648
  Resizer.panelWidth = leftSideRef.current.clientWidth
630
- window.addEventListener('pointerup', Resizer.onUp)
631
- window.addEventListener('pointermove', Resizer.onMove)
649
+ addEventListener('pointerup', Resizer.onUp)
650
+ addEventListener('pointermove', Resizer.onMove)
632
651
  document.body.style.userSelect = 'none'
633
652
  document.body.style.cursor = 'col-resize'
634
653
  }
@@ -641,8 +660,8 @@ Resizer.onMove = function (event) {
641
660
  })
642
661
  }
643
662
  Resizer.onUp = function () {
644
- window.removeEventListener('pointermove', Resizer.onMove)
645
- window.removeEventListener('pointerup', Resizer.onUp)
663
+ removeEventListener('pointermove', Resizer.onMove)
664
+ removeEventListener('pointerup', Resizer.onUp)
646
665
  cancelAnimationFrame(Resizer.raf)
647
666
  Resizer.raf = 0
648
667
  document.body.style.userSelect = 'auto'
@@ -860,9 +879,6 @@ function className(...args) {
860
879
 
861
880
 
862
881
  function createElement(tag, props, ...children) {
863
- if (typeof tag === 'function')
864
- return tag(props)
865
-
866
882
  const node = document.createElement(tag)
867
883
  for (const [k, v] of Object.entries(props || {}))
868
884
  if (k === 'ref') v.current = node
package/src/Mockaton.js CHANGED
@@ -6,12 +6,12 @@ import { config, setup } from './config.js'
6
6
  import { dispatchMock } from './MockDispatcher.js'
7
7
  import { dispatchStatic } from './StaticDispatcher.js'
8
8
  import * as staticCollection from './staticCollection.js'
9
- import { BodyReaderError } from './utils/http-request.js'
10
9
  import * as mockBrokerCollection from './mockBrokersCollection.js'
11
10
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
12
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
13
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
14
- import { sendNoContent, sendInternalServerError, sendUnprocessableContent } from './utils/http-response.js'
13
+ import { BodyReaderError, isControlCharFree } from './utils/http-request.js'
14
+ import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
17
17
  export function Mockaton(options) {
@@ -39,27 +39,34 @@ export function Mockaton(options) {
39
39
 
40
40
  async function onRequest(req, response) {
41
41
  response.on('error', logger.warn)
42
+ response.setHeader('Server', 'Mockaton')
43
+
44
+ const url = req.url || ''
45
+
46
+ if (url.length > 2048) {
47
+ sendTooLongURI(response)
48
+ return
49
+ }
50
+
51
+ if (!isControlCharFree(url)) {
52
+ sendBadRequest(response)
53
+ return
54
+ }
42
55
 
43
56
  try {
44
- response.setHeader('Server', 'Mockaton')
57
+ const { method } = req
45
58
 
46
59
  if (config.corsAllowed)
47
60
  setCorsHeaders(req, response, config)
48
61
 
49
- const { url, method } = req
50
-
51
62
  if (isPreflight(req))
52
63
  sendNoContent(response)
53
-
54
64
  else if (method === 'PATCH' && apiPatchRequests.has(url))
55
65
  await apiPatchRequests.get(url)(req, response)
56
-
57
66
  else if (method === 'GET' && apiGetRequests.has(url))
58
67
  apiGetRequests.get(url)(req, response)
59
-
60
68
  else if (method === 'GET' && staticCollection.brokerByRoute(url))
61
69
  await dispatchStatic(req, response)
62
-
63
70
  else
64
71
  await dispatchMock(req, response)
65
72
  }
@@ -48,3 +48,21 @@ export function readBody(req, parser = a => a) {
48
48
  }
49
49
  })
50
50
  }
51
+
52
+ export const reControlAndDelChars = /[\x00-\x1f\x7f]/
53
+ export function isControlCharFree(url) {
54
+ try {
55
+ const decoded = decode(url)
56
+ return decoded && !reControlAndDelChars.test(decoded)
57
+ }
58
+ catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ export function decode(url) {
64
+ const candidate = decodeURIComponent(url)
65
+ return candidate === decodeURIComponent(candidate)
66
+ ? candidate
67
+ : '' // reject multiple encodings
68
+ }
@@ -28,21 +28,33 @@ export function sendNoContent(response) {
28
28
  }
29
29
 
30
30
 
31
+ export function sendBadRequest(response) {
32
+ response.statusCode = 400
33
+ logger.access(response)
34
+ response.end()
35
+ }
36
+
31
37
  export function sendNotFound(response) {
32
38
  response.statusCode = 404
33
39
  logger.access(response)
34
40
  response.end()
35
41
  }
36
42
 
43
+ export function sendTooLongURI(response) {
44
+ response.statusCode = 414
45
+ logger.access(response)
46
+ response.end()
47
+ }
48
+
37
49
  export function sendUnprocessableContent(response, error) {
38
- logger.warn(error)
50
+ logger.access(response, error)
39
51
  response.statusCode = 422
40
52
  response.end(error)
41
53
  }
42
54
 
43
55
 
44
56
  export function sendInternalServerError(response, error) {
45
- logger.error(error?.message || error, error?.stack || undefined)
57
+ logger.error(500, logger.sanitizeURL(response.req.url), error?.message || error, error?.stack || '')
46
58
  response.statusCode = 500
47
59
  response.end()
48
60
  }
@@ -1,3 +1,6 @@
1
+ import { decode, reControlAndDelChars } from './http-request.js'
2
+
3
+
1
4
  export const logger = new class {
2
5
  #level = 'normal'
3
6
 
@@ -12,16 +15,17 @@ export const logger = new class {
12
15
 
13
16
  accessMock(url, ...msg) {
14
17
  if (this.#level !== 'quiet')
15
- console.log(this.#msg('MOCK', this.#sanitizeURL(url), ...msg))
18
+ console.log(this.#msg('MOCK', this.sanitizeURL(url), ...msg))
16
19
  }
17
20
 
18
- access(response) {
21
+ access(response, error) {
19
22
  if (this.#level === 'verbose')
20
23
  console.log(this.#msg(
21
24
  'ACCESS',
22
25
  response.req.method,
23
26
  response.statusCode,
24
- this.#sanitizeURL(response.req.url)))
27
+ this.sanitizeURL(response.req.url),
28
+ error))
25
29
  }
26
30
 
27
31
  warn(...msg) {
@@ -33,10 +37,22 @@ export const logger = new class {
33
37
  }
34
38
 
35
39
  #msg(...msg) {
40
+ if (!msg.at(-1))
41
+ msg.pop()
36
42
  return [new Date().toISOString(), ...msg].join('::')
37
43
  }
38
-
39
- #sanitizeURL(url) {
40
- return decodeURIComponent(url).replace(/[\x00-\x1F\x7F\x9B]/g, '')
44
+
45
+ sanitizeURL(url) {
46
+ try {
47
+ const decoded = decode(url)
48
+ if (!decoded)
49
+ return '__MULTI_ENCODED_URL__'
50
+ return decoded
51
+ .replace(reControlAndDelChars, '')
52
+ .slice(0, 200)
53
+ }
54
+ catch {
55
+ return '__NON_DECODABLE_URL__'
56
+ }
41
57
  }
42
58
  }