releasebird-javascript-sdk 1.0.58 → 1.0.62

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.
@@ -25,7 +25,17 @@
25
25
  "Bash(./add-ecr-policy.sh:*)",
26
26
  "Bash(aws elasticbeanstalk describe-environment-health:*)",
27
27
  "Bash(aws elasticbeanstalk describe-instances-health:*)",
28
- "Bash(node -e:*)"
28
+ "Bash(node -e:*)",
29
+ "Bash(node test-compression.js:*)",
30
+ "Bash(node test-deployed-service.js:*)",
31
+ "Bash(aws elasticbeanstalk restart-app-server:*)",
32
+ "Bash(aws ec2 describe-instances:*)",
33
+ "Bash(node test-java-backend.js:*)",
34
+ "Read(//Users/christianzillmann/IdeaProjects/releasebook-backend/src/main/java/io/releasebook/backend/web/rest/papi/**)",
35
+ "Read(//Users/christianzillmann/IdeaProjects/releasebook-backend/**)",
36
+ "Bash(node test-full-flow.js:*)",
37
+ "Bash(node test-emoji-simple.js:*)",
38
+ "Bash(node:*)"
29
39
  ],
30
40
  "deny": [],
31
41
  "ask": []
@@ -0,0 +1,204 @@
1
+ # Nginx Configuration for Large Screenshot Payloads
2
+
3
+ ## Problem
4
+ The backend-image-service is returning `413 Request Entity Too Large` errors because nginx is blocking large HTML payloads before they reach the backend service.
5
+
6
+ ## Solution
7
+ Configure nginx to accept larger request bodies by increasing `client_max_body_size`.
8
+
9
+ ---
10
+
11
+ ## For backend-image-service
12
+
13
+ ### Option 1: Direct nginx.conf Configuration
14
+
15
+ If you're managing nginx directly, add or update the following in your nginx configuration:
16
+
17
+ ```nginx
18
+ http {
19
+ # Set globally for all server blocks
20
+ client_max_body_size 50M; # Adjust size as needed (50MB recommended)
21
+
22
+ # ... other http config ...
23
+ }
24
+ ```
25
+
26
+ Or configure it per server block:
27
+
28
+ ```nginx
29
+ server {
30
+ listen 80;
31
+ server_name your-domain.com;
32
+
33
+ # Set for this specific server
34
+ client_max_body_size 50M;
35
+
36
+ location /images/screenshot/render {
37
+ # Or set for specific location only
38
+ client_max_body_size 50M;
39
+ proxy_pass http://backend-image-service:3000;
40
+
41
+ # Also consider these settings for large payloads:
42
+ proxy_read_timeout 300s;
43
+ proxy_connect_timeout 300s;
44
+ proxy_send_timeout 300s;
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Option 2: Docker/Kubernetes Configuration
50
+
51
+ #### Docker Compose
52
+ If using docker-compose with an nginx container:
53
+
54
+ ```yaml
55
+ services:
56
+ nginx:
57
+ image: nginx:1.28.0
58
+ volumes:
59
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
60
+ environment:
61
+ - NGINX_CLIENT_MAX_BODY_SIZE=50M
62
+ ```
63
+
64
+ #### Kubernetes ConfigMap
65
+ If using Kubernetes:
66
+
67
+ ```yaml
68
+ apiVersion: v1
69
+ kind: ConfigMap
70
+ metadata:
71
+ name: nginx-config
72
+ data:
73
+ client-max-body-size: "50m"
74
+ ---
75
+ apiVersion: v1
76
+ kind: ConfigMap
77
+ metadata:
78
+ name: nginx-configuration
79
+ namespace: ingress-nginx
80
+ data:
81
+ proxy-body-size: "50m"
82
+ client-max-body-size: "50m"
83
+ ```
84
+
85
+ ### Option 3: AWS Elastic Beanstalk
86
+
87
+ If your backend-image-service runs on Elastic Beanstalk, add `.ebextensions/nginx.config`:
88
+
89
+ ```yaml
90
+ files:
91
+ "/etc/nginx/conf.d/proxy.conf":
92
+ mode: "000644"
93
+ owner: root
94
+ group: root
95
+ content: |
96
+ client_max_body_size 50M;
97
+ ```
98
+
99
+ Or use `.platform/nginx/conf.d/proxy.conf`:
100
+
101
+ ```
102
+ client_max_body_size 50M;
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Recommended Size
108
+
109
+ - **10M**: For typical web pages with moderate styling
110
+ - **25M**: For complex pages with many images and styles
111
+ - **50M**: For very large/complex pages (recommended)
112
+ - **100M**: If you experience issues even with 50M
113
+
114
+ ## After Configuration
115
+
116
+ 1. **Reload nginx** to apply changes:
117
+ ```bash
118
+ sudo nginx -s reload
119
+ # or
120
+ sudo systemctl reload nginx
121
+ ```
122
+
123
+ 2. **Verify the configuration**:
124
+ ```bash
125
+ sudo nginx -t
126
+ ```
127
+
128
+ 3. **Test with a large payload** to confirm the 413 error is resolved
129
+
130
+ ---
131
+
132
+ ## Frontend Changes (Already Implemented)
133
+
134
+ The JavaScript SDK now:
135
+ - ✅ Compresses HTML payloads using gzip before sending
136
+ - ✅ Sends compressed data as `htmlCompressed` (base64-encoded)
137
+ - ✅ Includes `compressed: true` flag in the request
138
+ - ✅ Falls back to uncompressed if compression is unavailable
139
+ - ✅ Logs payload sizes for debugging
140
+
141
+ ## Backend Changes Required
142
+
143
+ The **backend-image-service** must be updated to handle compressed payloads:
144
+
145
+ ```javascript
146
+ // Example for Node.js backend
147
+ const zlib = require('zlib');
148
+
149
+ app.post('/images/screenshot/render', (req, res) => {
150
+ let html;
151
+
152
+ if (req.body.compressed && req.body.htmlCompressed) {
153
+ // Decompress the HTML
154
+ const compressedBuffer = Buffer.from(req.body.htmlCompressed, 'base64');
155
+ html = zlib.gunzipSync(compressedBuffer).toString('utf-8');
156
+ console.log(`Decompressed HTML: ${compressedBuffer.length} -> ${html.length} bytes`);
157
+ } else {
158
+ html = req.body.html;
159
+ }
160
+
161
+ // Continue with screenshot rendering...
162
+ });
163
+ ```
164
+
165
+ Or for Java/Spring backend:
166
+
167
+ ```java
168
+ import java.util.Base64;
169
+ import java.util.zip.GZIPInputStream;
170
+
171
+ @PostMapping("/images/screenshot/render")
172
+ public ResponseEntity<?> renderScreenshot(@RequestBody ScreenshotRequest request) {
173
+ String html;
174
+
175
+ if (request.isCompressed() && request.getHtmlCompressed() != null) {
176
+ // Decompress the HTML
177
+ byte[] compressed = Base64.getDecoder().decode(request.getHtmlCompressed());
178
+ try (GZIPInputStream gzis = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
179
+ html = new String(gzis.readAllBytes(), StandardCharsets.UTF_8);
180
+ }
181
+ } else {
182
+ html = request.getHtml();
183
+ }
184
+
185
+ // Continue with screenshot rendering...
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Verification
192
+
193
+ After implementing both changes:
194
+
195
+ 1. Check browser console for compression logs:
196
+ ```
197
+ [Screenshot] Original HTML size: 2500000 bytes
198
+ [Screenshot] Compression: 2500000 -> 250000 bytes (10%)
199
+ [Screenshot] Final payload size: 250500 bytes
200
+ ```
201
+
202
+ 2. Verify nginx accepts the request (no 413 error)
203
+
204
+ 3. Verify backend successfully decompresses and processes the HTML
@@ -0,0 +1,12 @@
1
+ files:
2
+ "/etc/nginx/conf.d/proxy.conf":
3
+ mode: "000644"
4
+ owner: root
5
+ group: root
6
+ content: |
7
+ client_max_body_size 50M;
8
+ client_body_buffer_size 1M;
9
+
10
+ container_commands:
11
+ 01_reload_nginx:
12
+ command: "service nginx reload || true"
@@ -9,6 +9,11 @@ RUN apt-get update && apt-get install -y \
9
9
  gnupg \
10
10
  ca-certificates \
11
11
  fonts-liberation \
12
+ fonts-noto-color-emoji \
13
+ fonts-noto-cjk \
14
+ fonts-noto-cjk-extra \
15
+ fonts-dejavu-core \
16
+ fontconfig \
12
17
  libasound2 \
13
18
  libatk-bridge2.0-0 \
14
19
  libatk1.0-0 \
@@ -41,6 +46,12 @@ RUN npm ci --only=production
41
46
  # Copy application
42
47
  COPY server.js ./
43
48
 
49
+ # Copy font configuration
50
+ COPY fonts.conf /etc/fonts/local.conf
51
+
52
+ # Rebuild font cache
53
+ RUN fc-cache -fv
54
+
44
55
  # Expose port
45
56
  EXPOSE 3000
46
57
 
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0"?>
2
+ <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
3
+ <fontconfig>
4
+ <!-- Enable emoji rendering -->
5
+ <match target="pattern">
6
+ <test qual="any" name="family">
7
+ <string>sans-serif</string>
8
+ </test>
9
+ <edit name="family" mode="append" binding="weak">
10
+ <string>Noto Color Emoji</string>
11
+ </edit>
12
+ </match>
13
+
14
+ <match target="pattern">
15
+ <test qual="any" name="family">
16
+ <string>serif</string>
17
+ </test>
18
+ <edit name="family" mode="append" binding="weak">
19
+ <string>Noto Color Emoji</string>
20
+ </edit>
21
+ </match>
22
+
23
+ <match target="pattern">
24
+ <test qual="any" name="family">
25
+ <string>monospace</string>
26
+ </test>
27
+ <edit name="family" mode="append" binding="weak">
28
+ <string>Noto Color Emoji</string>
29
+ </edit>
30
+ </match>
31
+
32
+ <!-- Always prefer color emoji -->
33
+ <alias>
34
+ <family>emoji</family>
35
+ <prefer>
36
+ <family>Noto Color Emoji</family>
37
+ </prefer>
38
+ </alias>
39
+ </fontconfig>
@@ -1,10 +1,11 @@
1
1
  const express = require('express');
2
2
  const puppeteer = require('puppeteer');
3
+ const zlib = require('zlib');
3
4
  const app = express();
4
5
  const port = process.env.PORT || 3000;
5
6
 
6
7
  // Middleware
7
- app.use(express.json({ limit: '50mb' })); // Large HTML payloads
8
+ app.use(express.json({ limit: '50mb' })); // Large HTML payloads (includes compressed)
8
9
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
9
10
 
10
11
  // CORS
@@ -33,7 +34,10 @@ async function initBrowser() {
33
34
  '--disable-accelerated-2d-canvas',
34
35
  '--no-first-run',
35
36
  '--no-zygote',
36
- '--disable-gpu'
37
+ '--disable-gpu',
38
+ '--font-render-hinting=none',
39
+ '--enable-font-antialiasing',
40
+ '--disable-lcd-text'
37
41
  ]
38
42
  });
39
43
  console.log('[Puppeteer] Browser launched successfully');
@@ -41,18 +45,51 @@ async function initBrowser() {
41
45
  return browser;
42
46
  }
43
47
 
48
+ /**
49
+ * Decompress base64-encoded gzip data
50
+ */
51
+ function decompressHtml(base64CompressedHtml) {
52
+ try {
53
+ const compressedBuffer = Buffer.from(base64CompressedHtml, 'base64');
54
+ const decompressedBuffer = zlib.gunzipSync(compressedBuffer);
55
+ const html = decompressedBuffer.toString('utf-8');
56
+ console.log(`[Screenshot] Decompressed: ${compressedBuffer.length} -> ${html.length} bytes (${Math.round(html.length / compressedBuffer.length * 100)}%)`);
57
+ return html;
58
+ } catch (error) {
59
+ console.error('[Screenshot] Decompression failed:', error);
60
+ throw new Error('Failed to decompress HTML: ' + error.message);
61
+ }
62
+ }
63
+
44
64
  /**
45
65
  * Render HTML to screenshot
46
66
  */
47
67
  app.post('/render', async (req, res) => {
48
68
  console.log('[Screenshot] Received render request');
49
69
 
50
- const { html, width, height, url } = req.body;
51
-
52
- if (!html) {
53
- return res.status(400).json({ error: 'HTML content is required' });
70
+ const { html, htmlCompressed, compressed, width, height, url } = req.body;
71
+
72
+ // Handle compressed or uncompressed HTML
73
+ let htmlContent;
74
+ if (compressed && htmlCompressed) {
75
+ console.log('[Screenshot] Processing compressed HTML...');
76
+ try {
77
+ htmlContent = decompressHtml(htmlCompressed);
78
+ } catch (error) {
79
+ return res.status(400).json({
80
+ error: 'Failed to decompress HTML',
81
+ message: error.message
82
+ });
83
+ }
84
+ } else if (html) {
85
+ console.log('[Screenshot] Processing uncompressed HTML...');
86
+ htmlContent = html;
87
+ } else {
88
+ return res.status(400).json({ error: 'HTML content is required (html or htmlCompressed)' });
54
89
  }
55
90
 
91
+ console.log(`[Screenshot] HTML size: ${htmlContent.length} bytes`);
92
+
56
93
  const viewportWidth = width || 1920;
57
94
  const viewportHeight = height || 1080;
58
95
 
@@ -74,21 +111,37 @@ app.post('/render', async (req, res) => {
74
111
 
75
112
  console.log(`[Screenshot] Viewport set to ${viewportWidth}x${viewportHeight}`);
76
113
 
77
- // Set content
78
- await page.setContent(html, {
114
+ // Inject emoji font CSS into HTML
115
+ const emojiCSS = `
116
+ <style>
117
+ * {
118
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif !important;
119
+ }
120
+ </style>
121
+ `;
122
+
123
+ // Insert emoji CSS into HTML
124
+ let enhancedHTML = htmlContent;
125
+ if (htmlContent.includes('<head>')) {
126
+ enhancedHTML = htmlContent.replace('<head>', '<head>' + emojiCSS);
127
+ } else if (htmlContent.includes('<html>')) {
128
+ enhancedHTML = htmlContent.replace('<html>', '<html><head>' + emojiCSS + '</head>');
129
+ }
130
+
131
+ // Set content (using enhanced htmlContent with emoji fonts)
132
+ await page.setContent(enhancedHTML, {
79
133
  waitUntil: 'networkidle0', // Wait for network to be idle
80
134
  timeout: 30000
81
135
  });
82
136
 
83
- console.log('[Screenshot] HTML content loaded');
137
+ console.log('[Screenshot] HTML content loaded with emoji font support');
84
138
 
85
139
  // Wait a bit for any animations/transitions
86
140
  await new Promise(resolve => setTimeout(resolve, 500));
87
141
 
88
- // Take screenshot
142
+ // Take screenshot (PNG for better emoji support)
89
143
  const screenshot = await page.screenshot({
90
- type: 'jpeg',
91
- quality: 90,
144
+ type: 'png',
92
145
  fullPage: false, // Only visible viewport
93
146
  encoding: 'base64'
94
147
  });
@@ -96,7 +149,7 @@ app.post('/render', async (req, res) => {
96
149
  console.log('[Screenshot] Screenshot captured successfully');
97
150
 
98
151
  // Return as data URL
99
- const dataUrl = `data:image/jpeg;base64,${screenshot}`;
152
+ const dataUrl = `data:image/png;base64,${screenshot}`;
100
153
 
101
154
  res.json({
102
155
  screenshot: dataUrl,
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script for compression/decompression
5
+ * Run with: node test-compression.js
6
+ */
7
+
8
+ const zlib = require('zlib');
9
+
10
+ // Sample HTML (similar to what the frontend sends)
11
+ const testHtml = `
12
+ <!DOCTYPE html>
13
+ <html>
14
+ <head>
15
+ <meta charset="UTF-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
+ <style>
18
+ body {
19
+ font-family: Arial, sans-serif;
20
+ padding: 20px;
21
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
22
+ }
23
+ h1 {
24
+ color: white;
25
+ text-align: center;
26
+ }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h1>Test Screenshot with Compression</h1>
31
+ <p>This is a test HTML document to verify compression works correctly.</p>
32
+ </body>
33
+ </html>
34
+ `;
35
+
36
+ console.log('=== Compression Test ===\n');
37
+ console.log(`Original HTML size: ${testHtml.length} bytes`);
38
+
39
+ // Compress like the frontend does
40
+ const compressed = zlib.gzipSync(Buffer.from(testHtml, 'utf-8'));
41
+ const base64Compressed = compressed.toString('base64');
42
+
43
+ console.log(`Compressed size: ${compressed.length} bytes`);
44
+ console.log(`Base64 compressed size: ${base64Compressed.length} bytes`);
45
+ console.log(`Compression ratio: ${Math.round(compressed.length / testHtml.length * 100)}%`);
46
+
47
+ // Decompress like the backend does
48
+ const decompressedBuffer = zlib.gunzipSync(Buffer.from(base64Compressed, 'base64'));
49
+ const decompressed = decompressedBuffer.toString('utf-8');
50
+
51
+ console.log(`\nDecompressed size: ${decompressed.length} bytes`);
52
+ console.log(`Match: ${decompressed === testHtml ? '✅ SUCCESS' : '❌ FAILED'}`);
53
+
54
+ // Test with image-service endpoint
55
+ console.log('\n=== Testing Image Service ===\n');
56
+
57
+ const http = require('http');
58
+
59
+ const testPayload = {
60
+ htmlCompressed: base64Compressed,
61
+ compressed: true,
62
+ width: 1280,
63
+ height: 720,
64
+ url: 'http://test.local'
65
+ };
66
+
67
+ const payloadJson = JSON.stringify(testPayload);
68
+ console.log(`Payload size: ${payloadJson.length} bytes`);
69
+
70
+ const options = {
71
+ hostname: 'localhost',
72
+ port: 3000,
73
+ path: '/render',
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'Content-Length': Buffer.byteLength(payloadJson)
78
+ }
79
+ };
80
+
81
+ console.log('Sending request to http://localhost:3000/render...\n');
82
+
83
+ const req = http.request(options, (res) => {
84
+ console.log(`Status: ${res.statusCode}`);
85
+
86
+ let data = '';
87
+
88
+ res.on('data', (chunk) => {
89
+ data += chunk;
90
+ });
91
+
92
+ res.on('end', () => {
93
+ try {
94
+ const response = JSON.parse(data);
95
+ if (response.screenshot) {
96
+ console.log('✅ Screenshot generated successfully!');
97
+ console.log(`Screenshot size: ${response.screenshot.length} characters`);
98
+ console.log(`Image dimensions: ${response.width}x${response.height}`);
99
+ } else if (response.error) {
100
+ console.error('❌ Error:', response.error);
101
+ if (response.message) {
102
+ console.error('Message:', response.message);
103
+ }
104
+ }
105
+ } catch (e) {
106
+ console.error('❌ Failed to parse response:', e.message);
107
+ console.error('Raw response:', data);
108
+ }
109
+ });
110
+ });
111
+
112
+ req.on('error', (error) => {
113
+ console.error('❌ Request failed:', error.message);
114
+ console.error('\n⚠️ Make sure the image service is running:');
115
+ console.error(' cd backend-image-service/image-service');
116
+ console.error(' npm start');
117
+ });
118
+
119
+ req.write(payloadJson);
120
+ req.end();
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test deployed image service with compression
5
+ */
6
+
7
+ const zlib = require('zlib');
8
+ const https = require('https');
9
+
10
+ const SERVICE_URL = 'https://image.releasebird.com';
11
+
12
+ // Test HTML
13
+ const testHtml = `
14
+ <!DOCTYPE html>
15
+ <html>
16
+ <head>
17
+ <meta charset="UTF-8">
18
+ <style>
19
+ body {
20
+ font-family: Arial, sans-serif;
21
+ padding: 40px;
22
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
23
+ color: white;
24
+ }
25
+ h1 {
26
+ text-align: center;
27
+ font-size: 48px;
28
+ }
29
+ .box {
30
+ background: rgba(255,255,255,0.1);
31
+ padding: 20px;
32
+ border-radius: 10px;
33
+ margin: 20px 0;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <h1>✅ Deployment Successful!</h1>
39
+ <div class="box">
40
+ <h2>Backend Image Service</h2>
41
+ <p>Version: v20251028-193852</p>
42
+ <p>Compression: Enabled</p>
43
+ </div>
44
+ </body>
45
+ </html>
46
+ `;
47
+
48
+ console.log('=== Testing Deployed Image Service ===\n');
49
+ console.log(`Service URL: ${SERVICE_URL}`);
50
+ console.log(`Original HTML: ${testHtml.length} bytes\n`);
51
+
52
+ // Compress HTML
53
+ const compressed = zlib.gzipSync(Buffer.from(testHtml, 'utf-8'));
54
+ const base64Compressed = compressed.toString('base64');
55
+
56
+ console.log(`Compressed: ${compressed.length} bytes (${Math.round(compressed.length / testHtml.length * 100)}%)`);
57
+ console.log(`Base64: ${base64Compressed.length} bytes\n`);
58
+
59
+ // Prepare request
60
+ const requestData = {
61
+ htmlCompressed: base64Compressed,
62
+ compressed: true,
63
+ width: 1280,
64
+ height: 720,
65
+ url: 'http://test.deployment.local'
66
+ };
67
+
68
+ const requestBody = JSON.stringify(requestData);
69
+ console.log(`Request payload: ${requestBody.length} bytes\n`);
70
+
71
+ const url = new URL(`${SERVICE_URL}/render`);
72
+ const options = {
73
+ hostname: url.hostname,
74
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
75
+ path: url.pathname,
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'Content-Length': Buffer.byteLength(requestBody)
80
+ }
81
+ };
82
+
83
+ console.log('Sending request...\n');
84
+
85
+ const req = (url.protocol === 'https:' ? https : require('http')).request(options, (res) => {
86
+ console.log(`Status: ${res.statusCode} ${res.statusMessage}`);
87
+ console.log(`Headers:`, res.headers);
88
+ console.log('');
89
+
90
+ let data = '';
91
+
92
+ res.on('data', (chunk) => {
93
+ data += chunk;
94
+ });
95
+
96
+ res.on('end', () => {
97
+ try {
98
+ const response = JSON.parse(data);
99
+
100
+ if (response.screenshot) {
101
+ console.log('✅ Screenshot generated successfully!');
102
+ console.log(`Screenshot data URL length: ${response.screenshot.length} characters`);
103
+ console.log(`Dimensions: ${response.width}x${response.height}`);
104
+ console.log('\n=== Test PASSED ===');
105
+ } else if (response.error) {
106
+ console.error('❌ Error from service:', response.error);
107
+ if (response.message) {
108
+ console.error('Message:', response.message);
109
+ }
110
+ console.log('\n=== Test FAILED ===');
111
+ }
112
+ } catch (e) {
113
+ console.error('❌ Failed to parse response:', e.message);
114
+ console.error('Raw response (first 500 chars):', data.substring(0, 500));
115
+ console.log('\n=== Test FAILED ===');
116
+ }
117
+ });
118
+ });
119
+
120
+ req.on('error', (error) => {
121
+ console.error('❌ Request failed:', error.message);
122
+ console.log('\n=== Test FAILED ===');
123
+ });
124
+
125
+ req.write(requestBody);
126
+ req.end();