From 05b6bf8c04c6fd09c5699a645fc90a3cfff9113a Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 6 Mar 2026 15:27:12 +0100 Subject: [PATCH] Replace remaining usage of phin (#862) Replace the remaining places in the codebase using phin with fetch. With the exception of HttpCloser which uses the http/https request method instead. --- package.json | 1 - packages/ctl/package.json | 1 - packages/ctl/src/commands_controller.ts | 8 ++---- packages/ctl/src/commands_instance.ts | 36 +++++++++++------------ packages/ctl/src/commands_mod.ts | 38 ++++++++++++------------- packages/host/package.json | 1 - packages/host/src/Host.ts | 38 ++++++++++++------------- packages/host/src/Instance.ts | 13 +++++---- plugins/subspace_storage/test/routes.js | 2 +- pnpm-workspace.yaml | 1 - test/controller/HttpCloser.js | 32 ++++++++++++--------- test/integration/clusterio.js | 8 ++---- test/integration/index.js | 14 ++++----- 13 files changed, 91 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index bb44f9cb..6b2eb80e 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "jszip": "^3.10.1", "mocha": "^11.7.1", "nyc": "^17.1.0", - "phin": "^3.7.1", "typedoc": "^0.28.9", "typescript": "^5.9.2", "yargs": "^18.0.0" diff --git a/packages/ctl/package.json b/packages/ctl/package.json index af4819ed..31d6c371 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -24,7 +24,6 @@ "set-blocking": "catalog:", "fs-extra": "catalog:", "winston": "catalog:", - "phin": "catalog:", "as-table": "^1.0.55", "yargs": "^17.7.2" }, diff --git a/packages/ctl/src/commands_controller.ts b/packages/ctl/src/commands_controller.ts index 9c2837f6..bb17cbc8 100644 --- a/packages/ctl/src/commands_controller.ts +++ b/packages/ctl/src/commands_controller.ts @@ -1,7 +1,6 @@ import fs from "fs-extra"; import asTableModule from "as-table"; import events from "events"; -import phin from "phin"; import os from "os"; import child_process from "child_process"; @@ -166,11 +165,8 @@ controllerPluginCommands.add(new lib.Command({ handler: async function(args: object, control: Control) { let url = new URL(control.config.get("control.controller_url")!); url.pathname += "api/plugins"; - let response = await phin<[]>({ - url, - parse: "json", - }); - print(asTable(response.body)); + let response = await fetch(url); + print(asTable(await response.json() as [])); }, })); controllerPluginCommands.add(new lib.Command({ diff --git a/packages/ctl/src/commands_instance.ts b/packages/ctl/src/commands_instance.ts index 20d5b92d..d7bfacda 100644 --- a/packages/ctl/src/commands_instance.ts +++ b/packages/ctl/src/commands_instance.ts @@ -2,7 +2,6 @@ import fs from "fs-extra"; import asTableModule from "as-table"; import events from "events"; import path from "path"; -import phin from "phin"; import os from "os"; import child_process from "child_process"; import stream from "stream"; @@ -330,8 +329,6 @@ instanceSaveCommands.add(new lib.Command({ if (!filename.endsWith(".zip")) { throw new lib.CommandError("Save name must end with .zip"); } - // phin doesn't support streaming requests :( - let content = await fs.readFile(args.filepath); let instanceId = await lib.resolveInstance(control, args.instance); let url = new URL(control.config.get("control.controller_url")!); @@ -339,29 +336,32 @@ instanceSaveCommands.add(new lib.Command({ url.searchParams.append("instance_id", String(instanceId)); url.searchParams.append("filename", filename); - let result = await phin<{ errors?: string[], request_errors?: string[], saves?: string[] }>({ - url, method: "POST", + const response = await fetch(url, { + method: "POST", + body: await fs.createReadStream(args.filepath), + duplex: "half", headers: { - "X-Access-Token": control.config.get("control.controller_token"), + "X-Access-Token": control.config.get("control.controller_token")!, "Content-Type": "application/zip", }, - data: content, - parse: "json", }); - for (let error of result.body.errors || []) { + type ResponseType = { errors?: string[], request_errors?: string[], saves?: string[] }; + const result = await response.json() as ResponseType; + + for (let error of result.errors || []) { logger.error(error); } - for (let requestError of result.body.request_errors || []) { + for (let requestError of result.request_errors || []) { logger.error(requestError); } - if (result.body.saves && result.body.saves.length) { - logger.info(`Successfully uploaded as ${result.body.saves[0]}`); + if (result.saves && result.saves.length) { + logger.info(`Successfully uploaded as ${result.saves[0]}`); } - if ((result.body.errors || []).length || (result.body.request_errors || []).length) { + if ((result.errors || []).length || (result.request_errors || []).length) { throw new lib.CommandError("Uploading save failed"); } }, @@ -417,10 +417,10 @@ instanceSaveCommands.add(new lib.Command({ let url = new URL(control.config.get("control.controller_url")!); url.pathname += `api/stream/${streamId}`; - let response = await phin({ - url, method: "GET", - stream: true, - }); + let response = await fetch(url); + if (!response.body) { + throw new lib.CommandError("Got empty response from server."); + } let writeStream; let tempFilename = args.save.replace(/(\.zip)?$/, ".tmp.zip"); @@ -437,7 +437,7 @@ instanceSaveCommands.add(new lib.Command({ } } } - response.pipe(writeStream); + stream.Readable.fromWeb(response.body).pipe(writeStream); await finished(writeStream); let filename = await lib.findUnusedName(".", args.save, ".zip"); diff --git a/packages/ctl/src/commands_mod.ts b/packages/ctl/src/commands_mod.ts index 1fff3f42..a7e9f11e 100644 --- a/packages/ctl/src/commands_mod.ts +++ b/packages/ctl/src/commands_mod.ts @@ -2,7 +2,6 @@ import fs from "fs-extra"; import asTableModule from "as-table"; import events from "events"; import path from "path"; -import phin from "phin"; import stream from "stream"; import util from "util"; @@ -145,39 +144,38 @@ modCommands.add(new lib.Command({ if (!filename.endsWith(".zip")) { throw new lib.CommandError("Mod filename must end with .zip"); } - // phin doesn't support streaming requests :( - let content = await fs.readFile(args.file); let url = new URL(control.config.get("control.controller_url")!); url.pathname += "api/upload-mod"; url.searchParams.append("filename", filename); - let result = await phin< - { errors?: [], request_errors?: [], mods: Parameters[0][]} - >({ - url, method: "POST", + let response = await fetch(url, { + method: "POST", + body: fs.createReadStream(args.file), + duplex: "half", headers: { - "X-Access-Token": control.config.get("control.controller_token"), + "X-Access-Token": control.config.get("control.controller_token")!, "Content-Type": "application/zip", }, - data: content, - parse: "json", }); - for (let error of result.body.errors || []) { + type ResponseType = { errors?: [], request_errors?: [], mods: Parameters[0][]}; + const result = await response.json() as ResponseType; + + for (let error of result.errors || []) { logger.error(error); } - for (let requestError of result.body.request_errors || []) { + for (let requestError of result.request_errors || []) { logger.error(requestError); } - if (result.body.mods && result.body.mods.length) { - const mod = lib.ModInfo.fromJSON(result.body.mods[0]); + if (result.mods && result.mods.length) { + const mod = lib.ModInfo.fromJSON(result.mods[0]); logger.info(`Successfully uploaded ${mod.filename}`); } - if ((result.body.errors || []).length || (result.body.request_errors || []).length) { + if ((result.errors || []).length || (result.request_errors || []).length) { throw new lib.CommandError("Uploading mod failed"); } }, @@ -196,10 +194,10 @@ modCommands.add(new lib.Command({ let url = new URL(control.config.get("control.controller_url")!); url.pathname += `api/stream/${streamId}`; - let response = await phin({ - url, method: "GET", - stream: true, - }); + let response = await fetch(url); + if (!response.body) { + throw new lib.CommandError("Got empty response from server"); + } let writeStream; let filename = `${args.name}_${args.modVersion}.zip`; @@ -217,7 +215,7 @@ modCommands.add(new lib.Command({ } } } - response.pipe(writeStream); + stream.Readable.fromWeb(response.body).pipe(writeStream); await finished(writeStream); await fs.rename(tempFilename, filename); diff --git a/packages/host/package.json b/packages/host/package.json index 638f1501..08fed73a 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -26,7 +26,6 @@ "set-blocking": "catalog:", "fs-extra": "catalog:", "jszip": "catalog:", - "phin": "catalog:", "winston": "catalog:", "winston-daily-rotate-file": "catalog:", "jimp": "^0.22.8", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 2b41decf..63476061 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -3,7 +3,6 @@ import path from "path"; import events from "events"; import pidusage from "pidusage"; import setBlocking from "set-blocking"; -import phin from "phin"; import stream from "stream"; import util from "util"; @@ -585,17 +584,17 @@ export default class Host extends lib.Link { let url = new URL(this.config.get("host.controller_url")); url.pathname += `api/stream/${streamId}`; - let response = await phin({ - url, method: "GET", - stream: true, - }); + let response = await fetch(url); + if (!response.body) { + throw new Error(`Empty body downloading stream ${streamId} from controller`); + } const file = lib.ModInfo.filename(mod.name, mod.version); const filePath = path.join(this.modStore.modsDirectory, file); const tempFilePath = filePath.replace(/(\.zip)?$/, ".tmp.zip"); const writeStream = fs.createWriteStream(tempFilePath, { flags: "w" }); await events.once(writeStream, "open"); - response.pipe(writeStream); + stream.Readable.fromWeb(response.body).pipe(writeStream); await finished(writeStream); await fs.rename(tempFilePath, filePath); @@ -1018,14 +1017,11 @@ export default class Host extends lib.Link { let url = new URL(this.config.get("host.controller_url")); url.pathname += `api/stream/${streamId}`; - let response = await phin({ - url, method: "GET", - stream: true, - }); + let response = await fetch(url); - if (response.statusCode !== 200) { - let content = await lib.readStream(response); - throw new lib.RequestError(`Stream returned ${response.statusCode}: ${content.toString()}`); + if (response.status !== 200 || !response.body) { + let body = Buffer.from(await response.arrayBuffer()); + throw new lib.RequestError(`Stream returned ${response.status}: ${body.toString()}`); } let savesDir = path.join(instance.path, "saves"); @@ -1044,7 +1040,7 @@ export default class Host extends lib.Link { } } } - response.pipe(writeStream); + stream.Readable.fromWeb(response.body).pipe(writeStream); await finished(writeStream); name = await lib.findUnusedName(savesDir, name, ".zip"); @@ -1059,10 +1055,11 @@ export default class Host extends lib.Link { checkRequestSaveName(name); let instance = this.getRequestInstance(instanceId); - let content: Buffer; + let content = fs.createReadStream(path.join(instance.path, "saves", name)); try { - // phin doesn't support streaming requests :( - content = await fs.readFile(path.join(instance.path, "saves", name)); + // TODO: Due to a bug in fs-extra the ready event is not emitted here. Replace + // "open" with "ready" when fs-extra is replaced with node's fs module. + await events.once(content, "open"); } catch (err: any) { if (err.code === "ENOENT") { throw new lib.RequestError(`${name} does not exist`); @@ -1072,9 +1069,10 @@ export default class Host extends lib.Link { let url = new URL(this.config.get("host.controller_url")); url.pathname += `api/stream/${streamId}`; - phin({ - url, method: "PUT", - data: content, + fetch(url, { + method: "PUT", + body: content, + duplex: "half", }).catch(err => { logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instanceId)); }); diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 57cf2e03..952470be 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -1,7 +1,6 @@ import fs from "fs-extra"; import path from "path"; import pidusage from "pidusage"; -import phin from "phin"; import util from "util"; import type { Static } from "@sinclair/typebox"; import { exec } from "child_process"; @@ -1292,16 +1291,18 @@ end`.replace(/\r?\n/g, " "); let url = new URL(this._host.config.get("host.controller_url")); url.pathname += "api/upload-export"; url.searchParams.set("mod_pack_id", String(this.activeModPack.id)); - let response = await phin({ - url, method: "PUT", - data: content, + let response = await fetch(url, { + method: "PUT", + body: content, + duplex: "half", headers: { "Content-Type": "application/zip", "x-access-token": this._host.config.get("host.controller_token"), }, }); - if (response.statusCode !== 200) { - throw Error(`Upload failed: ${response.statusCode} ${response.statusMessage}: ${response.body}`); + if (response.status !== 200) { + const body = Buffer.from(await response.arrayBuffer()); + throw Error(`Upload failed: ${response.status} ${response.statusText}: ${body.toString()}`); } } finally { diff --git a/plugins/subspace_storage/test/routes.js b/plugins/subspace_storage/test/routes.js index 2753b41e..fc4aac5b 100644 --- a/plugins/subspace_storage/test/routes.js +++ b/plugins/subspace_storage/test/routes.js @@ -9,7 +9,7 @@ describe("subspace_storage endpoints", function() { describe("GET /api/inventory", function() { it("should return the controllers current inventory", async function() { let res = await get("/api/inventory"); - let inventory = res.body; + let inventory = await res.json(); assert.equal(typeof inventory, "object", "Inventory should be an object"); return; // XXX Disabled until a suitable test interface is added. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 01c191a9..11bbe69b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,6 @@ catalog: "set-blocking": ^2.0.0 "jszip": ^3.10.1 "chalk": ^4.1.2 - "phin": ^3.7.0 "ws": ^8.18.1 catalogMode: strict diff --git a/test/controller/HttpCloser.js b/test/controller/HttpCloser.js index d630600b..d5e34122 100644 --- a/test/controller/HttpCloser.js +++ b/test/controller/HttpCloser.js @@ -4,8 +4,7 @@ const fs = require("fs-extra"); const http = require("http"); const https = require("https"); const util = require("util"); - -const phin = require("phin"); +const events = require("node:events"); const HttpCloser = require("@clusterio/controller/dist/node/src/HttpCloser").default; @@ -38,8 +37,16 @@ function serverSuite(proto) { } async function get(url, options = {}) { - let core = { ...clientOptions, ...(options.core || {}) }; - return await phin({ ...options, url, core }); + const request = api.request(url, { ...clientOptions, ...options }); + request.end(); + const [response] = await events.once(request, "response"); + response.body = await new Promise((resolve, reject) => { + const chunks = []; + response.on("data", chunk => { chunks.push(chunk); }); + response.on("error", err => { reject(err); }); + response.on("end", () => { resolve(Buffer.concat(chunks)); }); + }); + return response; } before(async function() { @@ -101,7 +108,7 @@ function serverSuite(proto) { }); let agent = new api.Agent({ keepAlive: true }); - let request = catched(get(addr, { core: { agent }})); + let request = catched(get(addr, { agent })); await waitForStage1; stage2(); await closer.close(); @@ -121,7 +128,7 @@ function serverSuite(proto) { }); let agent = new api.Agent({ keepAlive: true }); - let request = catched(get(addr, { core: { agent }})); + let request = catched(get(addr, { agent })); await waitForStage1; await closer.close(); @@ -145,7 +152,7 @@ function serverSuite(proto) { let close = catched(closer.close()); let agent = new api.Agent({ keepAlive: true }); - await assert.rejects(get(addr, { core: { agent }}), { code: "ECONNREFUSED" }); + await assert.rejects(get(addr, { agent }), { code: "ECONNREFUSED" }); let response = await request; assert.equal(response.statusCode, 200, "non-200 response status code"); @@ -165,7 +172,7 @@ function serverSuite(proto) { }); let agent = new api.Agent({ keepAlive: true }); - let request = catched(get(addr, { core: { agent }})); + let request = catched(get(addr, { agent })); await waitForStage1; stage2(); await closer.close(); @@ -186,8 +193,8 @@ function serverSuite(proto) { }); let agent = new api.Agent({ keepAlive: true, maxSockets: 1 }); - let request = catched(get(addr, { core: { agent }})); - let droppedRequest = catched(get(addr, { core: { agent }})); + let request = catched(get(addr, { agent })); + let droppedRequest = catched(get(addr, { agent })); await waitForStage1; stage2(); await closer.close(); @@ -212,10 +219,7 @@ function serverSuite(proto) { await closer.close(tick*2); stage2(); - // Due to a bug in phin [https://github.com/ethanent/phin/issues/59] - // the request Promise never settles. - // TODO: uncomment when bug is fixed in phin. - // await assert.rejects(request); + await assert.rejects(request); }); } diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 59782207..be234991 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -4,7 +4,6 @@ const fs = require("fs-extra"); const jwt = require("jsonwebtoken"); const path = require("path"); const events = require("events"); -const phin = require("phin"); const lib = require("@clusterio/lib"); const libBuildMod = require("@clusterio/lib/build_mod"); @@ -79,16 +78,15 @@ async function runWithAltHost(callback) { } async function uploadSave(instanceId, name, content) { - return await phin({ - url: `https://localhost:4443/api/upload-save?instance_id=${instanceId}&filename=${name}`, + const uploadUrl = `https://localhost:4443/api/upload-save?instance_id=${instanceId}&filename=${name}`; + return await fetch(uploadUrl, { method: "POST", - core: { rejectUnauthorized: false }, headers: { "X-Access-Token": controlToken, "Content-Type": "application/zip", "Content-Length": String(content.length), }, - data: content, + body: content, }); } diff --git a/test/integration/index.js b/test/integration/index.js index 696533e7..3a11620d 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -4,7 +4,6 @@ const path = require("path"); const fs = require("fs-extra"); const child_process = require("child_process"); const jwt = require("jsonwebtoken"); -const phin = require("phin"); const util = require("util"); const events = require("events"); @@ -148,13 +147,12 @@ function hasFactorio() { } async function get(urlPath) { - let res = await phin({ - method: "GET", - url: `https://localhost:4443${urlPath}`, - core: { rejectUnauthorized: false }, - }); - if (res.statusCode !== 200) { - throw new Error(`Got response code ${res.statusCode}, content: ${res.body}`); + const url = new URL("https://localhost:4443"); + url.pathname = urlPath; + let res = await fetch(url); + if (res.status !== 200) { + const body = Buffer.from(await res.arrayBuffer()); + throw new Error(`Got response code ${res.status}, content: ${body.toString()}`); } return res; }