fix(server): resolve config kv properly

This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent 4e510bf5c2
commit a70001f461
6 changed files with 124 additions and 6 deletions
@@ -0,0 +1,21 @@
import type { MiddlewareHandler } from 'hono'
import type { ConfigKVService } from '../services/config-kv'
import type { HonoEnv } from '../types/hono'
import { createServiceUnavailableError } from '../utils/error'
/**
* Middleware factory that checks required config keys exist in Redis.
* Returns 503 if any key is missing.
*/
export function configGuard(configKV: ConfigKVService, keys: Parameters<ConfigKVService['get']>[0][]): MiddlewareHandler<HonoEnv> {
return async (_c, next) => {
for (const key of keys) {
const value = await configKV.getOptional(key)
if (value === null)
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
}
await next()
}
}
+4 -1
View File
@@ -10,6 +10,7 @@ import { Hono } from 'hono'
import { integer, minValue, number, object, pipe, safeParse } from 'valibot'
import { authGuard } from '../middlewares/auth'
import { configGuard } from '../middlewares/config-guard'
import { createBadRequestError, createServiceUnavailableError } from '../utils/error'
const CheckoutBodySchema = object({
@@ -19,8 +20,10 @@ const CheckoutBodySchema = object({
export function createStripeRoutes(fluxService: FluxService, stripeService: StripeService, configKV: ConfigKVService, env: Env) {
const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null
const fluxConfigGuard = configGuard(configKV, ['FLUX_PER_CENT'])
return new Hono<HonoEnv>()
.post('/checkout', authGuard, async (c) => {
.post('/checkout', authGuard, fluxConfigGuard, async (c) => {
if (!stripe)
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
+2 -1
View File
@@ -8,6 +8,7 @@ import type { HonoEnv } from '../types/hono'
import { Hono } from 'hono'
import { authGuard } from '../middlewares/auth'
import { configGuard } from '../middlewares/config-guard'
import { createPaymentRequiredError } from '../utils/error'
// Only forward these headers from the upstream LLM response
@@ -53,7 +54,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
}
return new Hono<HonoEnv>()
.use('*', authGuard)
.use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST']))
.post('/chat/completions', handleCompletion)
.post('/chat/completion', handleCompletion)
}
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createConfigKVService } from '../config-kv'
function createMockRedis() {
const store = new Map<string, string>()
return {
get: vi.fn(async (key: string) => store.get(key) ?? null),
set: vi.fn(async (key: string, value: string) => { store.set(key, value) }),
_store: store,
}
}
describe('configKVService', () => {
let redis: ReturnType<typeof createMockRedis>
let service: ReturnType<typeof createConfigKVService>
beforeEach(() => {
redis = createMockRedis()
service = createConfigKVService(redis as any)
})
// --- get ---
it('get should throw 503 when key is not set', async () => {
await expect(service.get('FLUX_PER_CENT'))
.rejects
.toThrow('Config key "FLUX_PER_CENT" is not set in Redis')
})
it('get should return numeric value when key is set', async () => {
redis._store.set('config:FLUX_PER_CENT', '5')
const value = await service.get('FLUX_PER_CENT')
expect(value).toBe(5)
})
it('get should read from correct prefixed key', async () => {
redis._store.set('config:FLUX_PER_REQUEST', '3')
await service.get('FLUX_PER_REQUEST')
expect(redis.get).toHaveBeenCalledWith('config:FLUX_PER_REQUEST')
})
// --- getOptional ---
it('getOptional should return null when key is not set', async () => {
const value = await service.getOptional('FLUX_PER_CENT')
expect(value).toBeNull()
})
it('getOptional should return numeric value when key is set', async () => {
redis._store.set('config:INITIAL_USER_FLUX', '200')
const value = await service.getOptional('INITIAL_USER_FLUX')
expect(value).toBe(200)
})
// --- set ---
it('set should write value to Redis with prefix', async () => {
await service.set('FLUX_PER_CENT', 10)
expect(redis.set).toHaveBeenCalledWith('config:FLUX_PER_CENT', '10')
expect(redis._store.get('config:FLUX_PER_CENT')).toBe('10')
})
it('set then get should round-trip correctly', async () => {
await service.set('INITIAL_USER_FLUX', 500)
const value = await service.get('INITIAL_USER_FLUX')
expect(value).toBe(500)
})
})
+13 -2
View File
@@ -1,10 +1,21 @@
import { beforeAll, describe, expect, it } from 'vitest'
import type { createConfigKVService } from '../config-kv'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { mockDB } from '../../libs/mock-db'
import { createFluxService } from '../flux'
import * as schema from '../../schemas'
function createMockConfigKV(overrides: Record<string, number> = {}): ReturnType<typeof createConfigKVService> {
const defaults: Record<string, number> = { INITIAL_USER_FLUX: 100, FLUX_PER_CENT: 1, FLUX_PER_REQUEST: 1, ...overrides }
return {
get: vi.fn(async (key: string) => defaults[key]),
getOptional: vi.fn(async (key: string) => defaults[key] ?? null),
set: vi.fn(),
} as any
}
describe('fluxService', () => {
let db: any
let service: ReturnType<typeof createFluxService>
@@ -12,7 +23,7 @@ describe('fluxService', () => {
beforeAll(async () => {
db = await mockDB(schema)
service = createFluxService(db)
service = createFluxService(db, createMockConfigKV())
// Create a test user for foreign key constraints
const [user] = await db.insert(schema.user).values({
+10 -2
View File
@@ -12,14 +12,22 @@ const KEY_PREFIX = 'config:'
export function createConfigKVService(redis: Redis) {
return {
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
async getOptional<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K] | null> {
const raw = await redis.get(`${KEY_PREFIX}${key}`)
if (raw === null)
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
return null
return Number(raw) as ConfigDefinitions[K]
},
async get<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
const value = await this.getOptional(key)
if (value === null)
throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET')
return value
},
async set<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
await redis.set(`${KEY_PREFIX}${key}`, String(value))
},