Przeglądaj źródła

fix: stabilize article ssr api requests

main
leiyun 1 miesiąc temu
rodzic
commit
8d918e3400
5 zmienionych plików z 140 dodań i 28 usunięć
  1. +61
    -22
      app/composables/useApi.js
  2. +19
    -3
      app/composables/useArticle.js
  3. +1
    -1
      ecosystem.config.cjs
  4. +2
    -1
      nuxt.config.js
  5. +57
    -1
      restart.sh

+ 61
- 22
app/composables/useApi.js Wyświetl plik

@@ -84,29 +84,66 @@ export function useApi() {
return { timeStr, nonce }
}

/**
* SSR JSON 请求使用 Node 原生 http/https。
*
* 线上运行环境可能与 Nuxt 构建环境的 Node 小版本不同,Node 23 下曾出现
* `Headers is not a constructor`,导致服务端渲染无法取到文章数据。这里不经过
* fetch/ofetch/Headers,避免运行时兼容层影响 SEO 页面。
*/
async function fetchJson(url, options = {}) {
const response = await fetch(url, options)
const text = await response.text()
let result = null

if (text) {
try {
result = JSON.parse(text)
} catch {
const err = new Error(text || `请求失败 (${response.status})`)
err.code = response.status
throw err
}
const target = new URL(url)
const transport = target.protocol === 'https:'
? await import('node:https')
: await import('node:http')
const body = typeof options.body === 'string' ? options.body : ''
const headers = { ...(options.headers || {}) }
if (body && !Object.keys(headers).some(key => key.toLowerCase() === 'content-length')) {
headers['Content-Length'] = Buffer.byteLength(body)
}

if (!response.ok) {
const err = new Error(result?.msg || result?.message || `请求失败 (${response.status})`)
err.code = result?.code || response.status
err.response = result
throw err
}
return await new Promise((resolve, reject) => {
const req = transport.request(target, {
method: options.method || 'GET',
headers,
}, (response) => {
const chunks = []
response.on('data', chunk => chunks.push(Buffer.from(chunk)))
response.on('end', () => {
const status = Number(response.statusCode || 0)
const text = Buffer.concat(chunks).toString('utf8')
let result = null

if (text) {
try {
result = JSON.parse(text)
} catch {
const err = new Error(text || `请求失败 (${status})`)
err.code = status
reject(err)
return
}
}

if (status < 200 || status >= 300) {
const err = new Error(result?.msg || result?.message || `请求失败 (${status})`)
err.code = result?.code || status
err.response = result
reject(err)
return
}

resolve(result || {})
})
})

return result || {}
req.setTimeout(15000, () => {
req.destroy(new Error(`请求超时: ${target.origin}`))
})
req.on('error', reject)
if (body) req.write(body)
req.end()
})
}

/**
@@ -155,9 +192,12 @@ export function useApi() {
const fetchOptions = {
method,
headers: reqHeaders,
onResponseError({ response }) {
}

if (import.meta.client) {
fetchOptions.onResponseError = ({ response }) => {
console.error('[useApi] HTTP Error', response.status, url)
},
}
}

if (method === 'GET') {
@@ -168,7 +208,6 @@ export function useApi() {
if (allParams[k] === '') delete allParams[k]
})
const queryStr = new URLSearchParams(allParams).toString()
fetchOptions.query = allParams
// GET 请求通过 server proxy 时,参数在 query 里
const finalUrl = url + (queryStr ? '?' + queryStr : '')
const result = import.meta.server


+ 19
- 3
app/composables/useArticle.js Wyświetl plik

@@ -116,9 +116,25 @@ export async function useArticleDetail(colId) {
const detail = computed(() => data.value?.detail || {})
const canonical = computed(() => `https://tool.aionline.cc${articleUrl(detail.value)}`)

// 文章不存在 → 404,避免无效页面被搜索引擎收录
if (error.value || !data.value?.detail?.id) {
throw createError({ statusCode: 404, statusMessage: '文章不存在', fatal: true })
if (error.value) {
console.error('[article:ssr] load detail failed', {
id,
colId,
message: error.value.message,
code: error.value.code || error.value.statusCode,
cause: error.value.cause?.message,
})
throw createError({
statusCode: 502,
message: '文章加载失败,请稍后重试',
fatal: true,
cause: error.value,
})
}

// 仅接口成功但确实没有数据时返回 404,避免把网络异常误判为文章不存在。
if (!data.value?.detail?.id) {
throw createError({ statusCode: 404, message: '文章不存在', fatal: true })
}

await nuxtApp.runWithContext(() => {


+ 1
- 1
ecosystem.config.cjs Wyświetl plik

@@ -2,7 +2,7 @@ const appName = process.env.APP_NAME || 'pc_nuxt'
const host = process.env.HOST || '0.0.0.0'
const port = process.env.PORT || '6888'
const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc'
const apiServerBase = process.env.NUXT_API_SERVER_BASE || apiBase
const apiServerBase = process.env.NUXT_API_SERVER_BASE || 'http://127.0.0.1:16888'
const env = {
NODE_ENV: 'production',
HOST: host,


+ 2
- 1
nuxt.config.js Wyświetl plik

@@ -37,7 +37,8 @@ function resolveApiBase() {
}

function resolveServerApiBase() {
const value = String(process.env.NUXT_API_SERVER_BASE || apiBase).trim().replace(/\/+$/, '')
const defaultServerApiBase = isProduction ? 'http://127.0.0.1:16888' : apiBase
const value = String(process.env.NUXT_API_SERVER_BASE || defaultServerApiBase).trim().replace(/\/+$/, '')
let url
try {
url = new URL(value)


+ 57
- 1
restart.sh Wyświetl plik

@@ -9,7 +9,7 @@ set -Eeuo pipefail
APP_NAME="${APP_NAME:-pc_nuxt}"
BRANCH="${BRANCH:-}"
NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}"
NUXT_API_SERVER_BASE="${NUXT_API_SERVER_BASE:-$NUXT_PUBLIC_API_BASE}"
NUXT_API_SERVER_BASE="${NUXT_API_SERVER_BASE:-http://127.0.0.1:16888}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

export NUXT_PUBLIC_API_BASE
@@ -72,6 +72,60 @@ if (!['http:', 'https:'].includes(url.protocol)) {
NODE
}

verify_server_api() {
log "Verify SSR API connectivity"
node - "$NUXT_API_SERVER_BASE" <<'NODE'
const http = require('node:http')
const https = require('node:https')
const base = process.argv[2]
const target = new URL('/article/getlist', `${base.replace(/\/+$/, '')}/`)
const body = new URLSearchParams({
mac: 'pc-nuxt-deploy-check',
base_timestamp: String(Math.floor(Date.now() / 1000)),
client: '1',
client_ios: '0',
version: '1.0.0',
version_code: '1',
page_no: '1',
page_size: '1'
}).toString()
const transport = target.protocol === 'https:' ? https : http
const request = transport.request(target, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'Content-Length': Buffer.byteLength(body)
},
timeout: 10000
}, (response) => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8')
let result
try {
result = JSON.parse(text)
} catch {
console.error(`[pc_nuxt] SSR API returned invalid JSON (${response.statusCode}): ${text.slice(0, 200)}`)
process.exit(1)
}
if (response.statusCode < 200 || response.statusCode >= 300 || result.code !== 0) {
console.error(`[pc_nuxt] SSR API check failed (${response.statusCode}): ${result.msg || text.slice(0, 200)}`)
process.exit(1)
}
console.log(`[pc_nuxt] SSR API OK: ${base}`)
})
})
request.on('timeout', () => request.destroy(new Error('timeout')))
request.on('error', (error) => {
console.error(`[pc_nuxt] SSR API unavailable: ${base} (${error.message})`)
process.exit(1)
})
request.write(body)
request.end()
NODE
}

validate_tls_security() {
if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then
log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification."
@@ -139,6 +193,8 @@ main() {
log "API: $NUXT_PUBLIC_API_BASE"
log "Server API: $NUXT_API_SERVER_BASE"

verify_server_api

sync_latest_code
install_dependencies
build_app


Ładowanie…
Anuluj
Zapisz