impulse-api 3.0.9 → 3.0.10

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/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "test:integration": "nyc --reporter=lcov --reporter=text-summary mocha 'test/integration/**/*-test.js'",
15
15
  "test-server": "node ./test/integration/test-server.js"
16
16
  },
17
- "version": "3.0.9",
17
+ "version": "3.0.10",
18
18
  "engines": {
19
19
  "node": ">=22"
20
20
  },
package/src/server.js CHANGED
@@ -177,6 +177,40 @@ class Server {
177
177
  };
178
178
 
179
179
  function sendResponse(error, response) {
180
+ // Handle raw responses - send Buffer/string as-is without JSON serialization
181
+ // When rawResponse is true, only Buffer and string are sent raw via res.end().
182
+ // All other types fall through to default JSON serialization below.
183
+ if (route.rawResponse === true) {
184
+ if (response && !error) {
185
+ // Successful response - send raw if Buffer or string
186
+ if (Buffer.isBuffer(response) || typeof response === 'string') {
187
+ res.status(200).end(response);
188
+ return;
189
+ }
190
+ // Not raw-compatible, fall through to default JSON serialization
191
+ } else if (error && !response) {
192
+ // Error response - send raw if Buffer or string
193
+ if (Buffer.isBuffer(error) || typeof error === 'string') {
194
+ const statusCode = (typeof error === 'number' && !isNaN(error)) ? error : 400;
195
+ res.status(statusCode).end(error);
196
+ return;
197
+ }
198
+ // Not raw-compatible, fall through to default JSON serialization
199
+ } else if (error && response) {
200
+ // Status code with response (redirects, etc.)
201
+ if ((error === 302 || error === 301 || error === 307 || error === 308) && response.Location) {
202
+ res.status(error).header('Location', response.Location).end();
203
+ return;
204
+ } else if (Buffer.isBuffer(response) || typeof response === 'string') {
205
+ res.status(error).end(response);
206
+ return;
207
+ }
208
+ // Not raw-compatible, fall through to default JSON serialization
209
+ }
210
+ // If rawResponse is true but response is not Buffer/string, fall through to default
211
+ }
212
+
213
+ // Default JSON serialization behavior
180
214
  if (error instanceof Error) {
181
215
  res.status(error.statusCode ? error.statusCode : 400).send(error);
182
216
  } else if (error && !response) {
@@ -0,0 +1,259 @@
1
+ const Server = require('../../src/server');
2
+ const assert = require('assert');
3
+ const http = require('http');
4
+ const path = require('path');
5
+
6
+ describe('rawResponse HTTP Integration', () => {
7
+ let testServer;
8
+ let testRouteDir;
9
+ let testPort = 9999;
10
+
11
+ before(async () => {
12
+ testRouteDir = path.join(__dirname, 'routes');
13
+ // Create and start test server once for all tests (use 'dev' env so server actually starts)
14
+ testServer = new Server({
15
+ name: 'test-server',
16
+ routeDir: testRouteDir,
17
+ port: testPort,
18
+ env: 'dev',
19
+ services: {}
20
+ });
21
+ await testServer.init();
22
+ });
23
+
24
+ after(async () => {
25
+ // Clean up test server
26
+ if (testServer && testServer.http) {
27
+ await new Promise((resolve) => {
28
+ if (testServer.http.listening) {
29
+ testServer.http.close(() => resolve());
30
+ } else {
31
+ resolve();
32
+ }
33
+ });
34
+ }
35
+ });
36
+
37
+ it('should send raw Buffer response when rawResponse is true', async () => {
38
+ const response = await new Promise((resolve, reject) => {
39
+ const options = {
40
+ hostname: 'localhost',
41
+ port: testPort,
42
+ path: '/raw-response-buffer',
43
+ method: 'GET'
44
+ };
45
+
46
+ const req = http.request(options, (res) => {
47
+ let data = Buffer.alloc(0);
48
+ res.on('data', (chunk) => {
49
+ data = Buffer.concat([data, chunk]);
50
+ });
51
+ res.on('end', () => {
52
+ resolve({
53
+ statusCode: res.statusCode,
54
+ body: data,
55
+ headers: res.headers
56
+ });
57
+ });
58
+ });
59
+
60
+ req.on('error', reject);
61
+ req.end();
62
+ });
63
+
64
+ assert.strictEqual(response.statusCode, 200);
65
+ assert.strictEqual(Buffer.isBuffer(response.body), true);
66
+ assert.strictEqual(response.body.toString(), 'raw buffer response data');
67
+ // Should not have JSON content-type since it's raw
68
+ assert.strictEqual(response.headers['content-type'], undefined);
69
+ });
70
+
71
+ it('should send raw string response when rawResponse is true', async () => {
72
+ const response = await new Promise((resolve, reject) => {
73
+ const options = {
74
+ hostname: 'localhost',
75
+ port: testPort,
76
+ path: '/raw-response-string',
77
+ method: 'GET'
78
+ };
79
+
80
+ const req = http.request(options, (res) => {
81
+ let data = '';
82
+ res.on('data', (chunk) => {
83
+ data += chunk;
84
+ });
85
+ res.on('end', () => {
86
+ resolve({
87
+ statusCode: res.statusCode,
88
+ body: data,
89
+ headers: res.headers
90
+ });
91
+ });
92
+ });
93
+
94
+ req.on('error', reject);
95
+ req.end();
96
+ });
97
+
98
+ assert.strictEqual(response.statusCode, 200);
99
+ assert.strictEqual(typeof response.body, 'string');
100
+ assert.strictEqual(response.body, 'raw string response data');
101
+ // Should not have JSON content-type since it's raw
102
+ assert.strictEqual(response.headers['content-type'], undefined);
103
+ });
104
+
105
+ it('should send JSON serialized response when rawResponse is false', async () => {
106
+ const response = await new Promise((resolve, reject) => {
107
+ const options = {
108
+ hostname: 'localhost',
109
+ port: testPort,
110
+ path: '/json-response',
111
+ method: 'GET'
112
+ };
113
+
114
+ const req = http.request(options, (res) => {
115
+ let data = '';
116
+ res.on('data', (chunk) => {
117
+ data += chunk;
118
+ });
119
+ res.on('end', () => {
120
+ resolve({
121
+ statusCode: res.statusCode,
122
+ body: data,
123
+ headers: res.headers
124
+ });
125
+ });
126
+ });
127
+
128
+ req.on('error', reject);
129
+ req.end();
130
+ });
131
+
132
+ assert.strictEqual(response.statusCode, 200);
133
+ const responseBody = JSON.parse(response.body);
134
+ assert.strictEqual(responseBody.success, true);
135
+ assert.strictEqual(responseBody.message, 'This is a JSON response');
136
+ assert.deepStrictEqual(responseBody.data, { test: 'value' });
137
+ // Should have JSON content-type when serialized
138
+ assert.ok(response.headers['content-type'].includes('application/json'));
139
+ });
140
+
141
+ it('should send JSON serialized response when rawResponse is true but object is returned', async () => {
142
+ const response = await new Promise((resolve, reject) => {
143
+ const options = {
144
+ hostname: 'localhost',
145
+ port: testPort,
146
+ path: '/raw-response-object',
147
+ method: 'GET'
148
+ };
149
+
150
+ const req = http.request(options, (res) => {
151
+ let data = '';
152
+ res.on('data', (chunk) => {
153
+ data += chunk;
154
+ });
155
+ res.on('end', () => {
156
+ resolve({
157
+ statusCode: res.statusCode,
158
+ body: data,
159
+ headers: res.headers
160
+ });
161
+ });
162
+ });
163
+
164
+ req.on('error', reject);
165
+ req.end();
166
+ });
167
+
168
+ assert.strictEqual(response.statusCode, 200);
169
+ // Even with rawResponse: true, objects should fall through to JSON serialization
170
+ const responseBody = JSON.parse(response.body);
171
+ assert.strictEqual(responseBody.success, true);
172
+ assert.strictEqual(responseBody.message, 'This should be JSON serialized');
173
+ // Should have JSON content-type when serialized
174
+ assert.ok(response.headers['content-type'].includes('application/json'));
175
+ });
176
+
177
+ it('should handle raw response with route parameters', async () => {
178
+ const testBody = JSON.stringify({ prefix: 'PREFIX' });
179
+ const response = await new Promise((resolve, reject) => {
180
+ const options = {
181
+ hostname: 'localhost',
182
+ port: testPort,
183
+ path: '/raw-response-params',
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ 'Content-Length': Buffer.byteLength(testBody)
188
+ }
189
+ };
190
+
191
+ const req = http.request(options, (res) => {
192
+ let data = '';
193
+ res.on('data', (chunk) => {
194
+ data += chunk;
195
+ });
196
+ res.on('end', () => {
197
+ resolve({
198
+ statusCode: res.statusCode,
199
+ body: data,
200
+ headers: res.headers
201
+ });
202
+ });
203
+ });
204
+
205
+ req.on('error', reject);
206
+ req.write(testBody);
207
+ req.end();
208
+ });
209
+
210
+ assert.strictEqual(response.statusCode, 200);
211
+ assert.strictEqual(typeof response.body, 'string');
212
+ assert.strictEqual(response.body, 'PREFIX: raw response with params');
213
+ // Should not have JSON content-type since it's raw
214
+ assert.strictEqual(response.headers['content-type'], undefined);
215
+ });
216
+
217
+ it('should handle both rawBody and rawResponse together', async () => {
218
+ const testBody = 'raw request body';
219
+ const response = await new Promise((resolve, reject) => {
220
+ const options = {
221
+ hostname: 'localhost',
222
+ port: testPort,
223
+ path: '/webhook',
224
+ method: 'POST',
225
+ headers: {
226
+ 'Content-Type': 'application/json',
227
+ 'Content-Length': Buffer.byteLength(testBody)
228
+ }
229
+ };
230
+
231
+ const req = http.request(options, (res) => {
232
+ let data = '';
233
+ res.on('data', (chunk) => {
234
+ data += chunk;
235
+ });
236
+ res.on('end', () => {
237
+ resolve({
238
+ statusCode: res.statusCode,
239
+ body: data,
240
+ headers: res.headers
241
+ });
242
+ });
243
+ });
244
+
245
+ req.on('error', reject);
246
+ req.write(testBody);
247
+ req.end();
248
+ });
249
+
250
+ assert.strictEqual(response.statusCode, 200);
251
+ // The webhook route returns JSON (doesn't have rawResponse), so verify JSON
252
+ const responseBody = JSON.parse(response.body);
253
+ assert.strictEqual(responseBody.success, true);
254
+ assert.strictEqual(responseBody.isBuffer, true);
255
+ // Original functionality: rawBody works for requests
256
+ assert.strictEqual(responseBody.rawBodyString, testBody);
257
+ });
258
+ });
259
+
@@ -0,0 +1,65 @@
1
+ exports.rawResponseBuffer = {
2
+ name: 'rawResponseBuffer',
3
+ method: 'get',
4
+ endpoint: '/raw-response-buffer',
5
+ rawResponse: true,
6
+ run: (services, inputs, next) => {
7
+ const buffer = Buffer.from('raw buffer response data');
8
+ next(null, buffer);
9
+ }
10
+ };
11
+
12
+ exports.rawResponseString = {
13
+ name: 'rawResponseString',
14
+ method: 'get',
15
+ endpoint: '/raw-response-string',
16
+ rawResponse: true,
17
+ run: (services, inputs, next) => {
18
+ next(null, 'raw string response data');
19
+ }
20
+ };
21
+
22
+ exports.rawResponseWithParams = {
23
+ name: 'rawResponseWithParams',
24
+ method: 'post',
25
+ endpoint: '/raw-response-params',
26
+ rawResponse: true,
27
+ inputs: {
28
+ prefix: {
29
+ required: true
30
+ }
31
+ },
32
+ run: (services, inputs, next) => {
33
+ const response = `${inputs.prefix}: raw response with params`;
34
+ next(null, response);
35
+ }
36
+ };
37
+
38
+ exports.jsonResponse = {
39
+ name: 'jsonResponse',
40
+ method: 'get',
41
+ endpoint: '/json-response',
42
+ rawResponse: false,
43
+ run: (services, inputs, next) => {
44
+ next(null, {
45
+ success: true,
46
+ message: 'This is a JSON response',
47
+ data: { test: 'value' }
48
+ });
49
+ }
50
+ };
51
+
52
+ exports.rawResponseObject = {
53
+ name: 'rawResponseObject',
54
+ method: 'get',
55
+ endpoint: '/raw-response-object',
56
+ rawResponse: true,
57
+ run: (services, inputs, next) => {
58
+ // Even with rawResponse: true, objects should fall through to JSON serialization
59
+ next(null, {
60
+ success: true,
61
+ message: 'This should be JSON serialized'
62
+ });
63
+ }
64
+ };
65
+
@@ -596,7 +596,166 @@ describe('server-test', () => {
596
596
  assert.strictEqual(res.statusCode, 200);
597
597
  });
598
598
 
599
- it('should process inputs alongside rawBody when both are present', async () => {
599
+ it('should send raw response as Buffer when route.rawResponse is true', async () => {
600
+ const Api = new Server({
601
+ name: 'test-Server',
602
+ routeDir: './test-routes',
603
+ port: 4000,
604
+ env: 'test',
605
+ services: {}
606
+ });
607
+
608
+ const testBody = { test: 'data' };
609
+ const testResponse = Buffer.from('raw response data');
610
+ const req = {
611
+ body: testBody,
612
+ query: {},
613
+ params: {},
614
+ files: {},
615
+ headers: {},
616
+ get: (header) => {
617
+ if (header === 'origin') return 'http://localhost:4000';
618
+ if (header === 'host') return 'localhost:4000';
619
+ return null;
620
+ }
621
+ };
622
+ const res = {
623
+ statusCode: 200,
624
+ status: (code) => {
625
+ res.statusCode = code;
626
+ return res;
627
+ },
628
+ send: (data) => {
629
+ res.sentData = data;
630
+ },
631
+ end: (data) => {
632
+ res.sentData = data;
633
+ }
634
+ };
635
+
636
+ const route = {
637
+ name: 'test-raw-response',
638
+ method: 'post',
639
+ endpoint: '/test',
640
+ rawResponse: true,
641
+ run: (services, inputs, next) => {
642
+ next(null, testResponse);
643
+ }
644
+ };
645
+
646
+ await Api.preprocessor(route, req, res);
647
+ assert.strictEqual(res.statusCode, 200);
648
+ assert.strictEqual(Buffer.isBuffer(res.sentData), true);
649
+ assert.deepStrictEqual(res.sentData, testResponse);
650
+ });
651
+
652
+ it('should send raw response as string when route.rawResponse is true', async () => {
653
+ const Api = new Server({
654
+ name: 'test-Server',
655
+ routeDir: './test-routes',
656
+ port: 4000,
657
+ env: 'test',
658
+ services: {}
659
+ });
660
+
661
+ const testBody = { test: 'data' };
662
+ const testResponse = 'raw string response';
663
+ const req = {
664
+ body: testBody,
665
+ query: {},
666
+ params: {},
667
+ files: {},
668
+ headers: {},
669
+ get: (header) => {
670
+ if (header === 'origin') return 'http://localhost:4000';
671
+ if (header === 'host') return 'localhost:4000';
672
+ return null;
673
+ }
674
+ };
675
+ const res = {
676
+ statusCode: 200,
677
+ status: (code) => {
678
+ res.statusCode = code;
679
+ return res;
680
+ },
681
+ send: (data) => {
682
+ res.sentData = data;
683
+ },
684
+ end: (data) => {
685
+ res.sentData = data;
686
+ }
687
+ };
688
+
689
+ const route = {
690
+ name: 'test-raw-response-string',
691
+ method: 'post',
692
+ endpoint: '/test',
693
+ rawResponse: true,
694
+ run: (services, inputs, next) => {
695
+ next(null, testResponse);
696
+ }
697
+ };
698
+
699
+ await Api.preprocessor(route, req, res);
700
+ assert.strictEqual(res.statusCode, 200);
701
+ assert.strictEqual(typeof res.sentData, 'string');
702
+ assert.strictEqual(res.sentData, testResponse);
703
+ });
704
+
705
+ it('should JSON serialize response when route.rawResponse is false', async () => {
706
+ const Api = new Server({
707
+ name: 'test-Server',
708
+ routeDir: './test-routes',
709
+ port: 4000,
710
+ env: 'test',
711
+ services: {}
712
+ });
713
+
714
+ const testBody = { test: 'data' };
715
+ const testResponse = { success: true, data: 'test' };
716
+ const req = {
717
+ body: testBody,
718
+ query: {},
719
+ params: {},
720
+ files: {},
721
+ headers: {},
722
+ get: (header) => {
723
+ if (header === 'origin') return 'http://localhost:4000';
724
+ if (header === 'host') return 'localhost:4000';
725
+ return null;
726
+ }
727
+ };
728
+ const res = {
729
+ statusCode: 200,
730
+ status: (code) => {
731
+ res.statusCode = code;
732
+ return res;
733
+ },
734
+ send: (data) => {
735
+ res.sentData = data;
736
+ },
737
+ end: (data) => {
738
+ res.sentData = data;
739
+ }
740
+ };
741
+
742
+ const route = {
743
+ name: 'test-json-response',
744
+ method: 'post',
745
+ endpoint: '/test',
746
+ rawResponse: false,
747
+ run: (services, inputs, next) => {
748
+ next(null, testResponse);
749
+ }
750
+ };
751
+
752
+ await Api.preprocessor(route, req, res);
753
+ assert.strictEqual(res.statusCode, 200);
754
+ // When rawResponse is false, Express will JSON serialize objects
755
+ assert.deepStrictEqual(res.sentData, testResponse);
756
+ });
757
+
758
+ it('should process inputs alongside rawResponse when both are present', async () => {
600
759
  const Api = new Server({
601
760
  name: 'test-Server',
602
761
  routeDir: './test-routes',