redis_hf / src /server.js
tudragon154203
fix: flatten ioredis HGETALL objects to [key,val,...] for upstash compat
cf11bfd
Raw
History Blame Contribute Delete
5.1 kB
import Fastify from "fastify";
import Redis from "ioredis";
const BLOCKED_COMMANDS = new Set([
"flushall", "config", "shutdown", "save", "bgsave", "client", "monitor",
"debug", "cluster", "replicaof", "slaveof", "replconf", "pfdebug",
]);
const ALLOWED_COMMANDS = new Set([
"get", "set", "del", "exists", "expire", "ttl", "incr", "decr",
"mget", "mset", "hget", "hset", "hdel", "hgetall",
"lpush", "rpush", "lpop", "rpop", "lrange",
"sadd", "srem", "smembers",
"zadd", "zrem", "zrange",
"ping",
"scan", "keys", "flushdb", "setex", "psetex", "getdel", "getex",
"zcard", "zscore", "zrevrange",
"xadd", "xread",
"setnx", "getset", "append", "strlen", "incrby", "decrby",
"expireat", "pexpire", "pttl", "persist",
"rename", "type", "lindex", "llen", "lrem", "ltrim",
"hexists", "hincrby", "hincrbyfloat", "hkeys", "hvals", "hlen", "hmget", "hmset",
"sismember", "scard", "sdiff", "sinter", "sunion",
"zrank", "zcount", "zincrby", "zremrangebyrank", "zremrangebyscore",
"srandmember", "spop",
]);
const API_TOKEN = process.env.REDIS_REST_TOKEN || process.env.API_TOKEN;
const REDIS_URL = process.env.INTERNAL_REDIS_URL || "redis://127.0.0.1:6379";
const PORT = parseInt(process.env.PORT || "7860", 10);
const HOST = process.env.HOST || "0.0.0.0";
const BODY_LIMIT = parseInt(process.env.MAX_BODY_SIZE || "5242880", 10);
const redis = new Redis(REDIS_URL, {
lazyConnect: true,
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 200, 3000),
commandTimeout: 5000,
});
redis.on("error", (err) => console.error("[redis]", err.message));
const app = Fastify({
bodyLimit: BODY_LIMIT,
logger: {
level: "info",
serializers: {
req: (req) => ({ method: req.method, url: req.url, remoteAddress: req.ip }),
},
},
});
function formatResult(raw) {
if (raw === null || raw === undefined) return null;
if (typeof raw === "bigint") return Number(raw);
if (Buffer.isBuffer(raw)) return raw.toString("utf-8");
if (Array.isArray(raw)) return raw.map(formatResult);
// ioredis returns JS objects for HGETALL etc; flatten to [key,val,...] for upstash compat
if (typeof raw === "object" && raw.constructor === Object) {
const flat = [];
for (const [k, v] of Object.entries(raw)) {
flat.push(k, String(v));
}
return flat;
}
return raw;
}
async function executeCommand(raw) {
if (!Array.isArray(raw) || raw.length === 0) return { error: "Invalid command format" };
const cmd = String(raw[0]).toLowerCase();
const args = raw.slice(1).map(String);
if (BLOCKED_COMMANDS.has(cmd)) return { error: `Command '${cmd.toUpperCase()}' is blocked` };
if (!ALLOWED_COMMANDS.has(cmd)) return { error: `Unsupported command: ${cmd.toUpperCase()}` };
try {
const result = await redis.call(cmd, ...args);
return { result: formatResult(result) };
} catch (err) {
return { error: err.message };
}
}
function isAuthed(request) {
if (!API_TOKEN) return true;
const header = request.headers.authorization;
if (!header) return false;
const m = header.match(/^Bearer\s+(.+)$/i);
return m ? m[1] === API_TOKEN : false;
}
function send401(reply) {
reply.code(401);
return { error: "Unauthorized" };
}
// Health check — no auth
app.get("/health", async () => {
try {
await redis.ping();
return { status: "ok", redis: "connected" };
} catch {
return { status: "error", redis: "disconnected" };
}
});
// Root GET — service info, no auth
app.get("/", async () => ({ service: "hf-redis-rest", status: "running" }));
// Path-style: GET /get/key, GET /set/key/value, etc.
app.get("/:command/*", async (request, reply) => {
if (!isAuthed(request)) return send401(reply);
const parts = request.url.slice(1).split("/").filter(Boolean);
return executeCommand(parts);
});
// Pipeline: POST /pipeline
app.post("/pipeline", async (request, reply) => {
if (!isAuthed(request)) return send401(reply);
const body = request.body;
if (!Array.isArray(body)) {
reply.code(400);
return { error: "Request body must be an array of commands" };
}
const results = [];
for (const cmd of body) {
results.push(await executeCommand(cmd));
}
return results;
});
// Main command endpoint: POST /
app.post("/", async (request, reply) => {
if (!isAuthed(request)) return send401(reply);
const body = request.body;
if (!body) return { service: "hf-redis-rest", status: "running" };
if (!Array.isArray(body)) {
reply.code(400);
return { error: "Request body must be a JSON array" };
}
// Upstash pipeline: array of command arrays
if (Array.isArray(body[0])) {
const results = [];
for (const cmd of body) results.push(await executeCommand(cmd));
return results;
}
return executeCommand(body);
});
async function start() {
try {
await redis.connect();
console.log("[redis] connected to", REDIS_URL);
await app.listen({ port: PORT, host: HOST });
console.log("[server] listening on", `${HOST}:${PORT}`);
} catch (err) {
console.error("[fatal]", err);
process.exit(1);
}
}
start();