diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json index ec226491..785dcafc 100644 --- a/apps/backend/data/khhao-sync-state.json +++ b/apps/backend/data/khhao-sync-state.json @@ -1,11 +1,11 @@ { "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", - "lastRunStartedAt": "2026-05-02T13:40:21.932Z", - "lastRunFinishedAt": "2026-05-02T13:40:23.162Z", + "lastRunStartedAt": "2026-05-02T13:46:11.938Z", + "lastRunFinishedAt": "2026-05-02T13:46:13.175Z", "lastRunStatus": "success", "lastErrorMessage": "", "fetchedCount": 40, "syncedCount": 0, "ignoredCount": 40, - "lastOrderCreatedAt": "2026-05-02T13:39:40.000Z" + "lastOrderCreatedAt": "2026-05-02T13:46:02.000Z" } diff --git a/apps/backend/src/services/platforms/cloudtentacles/http-client.js b/apps/backend/src/services/platforms/cloudtentacles/http-client.js index 443c26cd..7447ecf6 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/http-client.js +++ b/apps/backend/src/services/platforms/cloudtentacles/http-client.js @@ -1,5 +1,8 @@ // @ts-check +import http from 'node:http' +import https from 'node:https' + import { createHttpError } from '../../../utils/http.js' import { logInfo } from '../../../utils/logger.js' import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './shared.js' @@ -20,14 +23,14 @@ export async function cloudtentaclesRequest(pathname, options = {}) { }) try { - const response = await fetch(url, { + const response = await requestViaNodeHttp(url, { method, headers, body: normalizeRequestBody(options.body, headers['content-type']), signal: controller.signal, }) - const rawText = await response.text() + const rawText = response.bodyText const payload = tryParseJson(rawText) if (!response.ok) { @@ -105,3 +108,75 @@ function tryParseJson(text) { return null } } + +async function requestViaNodeHttp(url, { method, headers, body, signal }) { + const isHttps = url.protocol === 'https:' + const transport = isHttps ? https : http + + return new Promise((resolve, reject) => { + const request = transport.request(url, { + method, + headers, + rejectUnauthorized: false, + }, (response) => { + const chunks = [] + + response.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + }) + + response.on('end', () => { + const bodyText = Buffer.concat(chunks).toString('utf8') + resolve({ + ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, + status: Number(response.statusCode || 0), + headers: createHeaderAdapter(response.headers), + bodyText, + }) + }) + }) + + request.on('error', reject) + + if (signal) { + if (signal.aborted) { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + } else { + signal.addEventListener('abort', () => { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + }, { once: true }) + } + } + + if (typeof body !== 'undefined') { + request.write(body) + } + + request.end() + }) +} + +function createHeaderAdapter(headers) { + const normalized = new Map() + + for (const [key, value] of Object.entries(headers || {})) { + if (Array.isArray(value)) { + normalized.set(String(key || '').toLowerCase(), value.join(', ')) + continue + } + + if (typeof value === 'string') { + normalized.set(String(key || '').toLowerCase(), value) + } + } + + return { + get(name) { + return normalized.get(String(name || '').toLowerCase()) || null + }, + } +}