Changes made as recommended by Claude:

1. Added a request timeout to prevent server resources from being tied up (route.ts)
2. Implemented runtime validation for the API response shape (url-utils.ts)
3. Removed hardcoded English error messages and replaced them with localized strings (url-input-dialog.tsx)
4. Fixed the incorrect i18n namespace (changed from pdf.* to url.*) (url-input-dialog.tsx and en/ja/zh.json)
This commit is contained in:
Biki Kalita
2026-01-05 14:51:24 +05:30
parent 64268b0fac
commit 580d42f535
8 changed files with 1625 additions and 55 deletions

View File

@@ -3,6 +3,7 @@ import { NextResponse } from "next/server"
import TurndownService from "turndown"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000
// SSRF protection - block private/internal addresses
function isPrivateUrl(urlString: string): boolean {
@@ -84,12 +85,31 @@ export async function POST(req: Request) {
)
}
// Extract article content
const article = await extract(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; NextAIDrawio/1.0)",
},
})
// Extract article content with timeout to avoid tying up server resources
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, EXTRACT_TIMEOUT_MS)
let article
try {
article = await extract(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; NextAIDrawio/1.0)",
},
signal: controller.signal,
})
} catch (err: any) {
if (err?.name === "AbortError") {
return NextResponse.json(
{ error: "Timed out while fetching URL content" },
{ status: 504 },
)
}
throw err
} finally {
clearTimeout(timeoutId)
}
if (!article || !article.content) {
return NextResponse.json(

View File

@@ -60,9 +60,9 @@ export function UrlInputDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{dict.pdf.title}</DialogTitle>
<DialogTitle>{dict.url.title}</DialogTitle>
<DialogDescription>
{dict.pdf.description}
{dict.url.description}
</DialogDescription>
</DialogHeader>
@@ -91,7 +91,7 @@ export function UrlInputDialog({
onClick={() => onOpenChange(false)}
disabled={isExtracting}
>
{dict.pdf.Cancel}
{dict.url.Cancel}
</Button>
<Button
onClick={handleSubmit}
@@ -100,12 +100,12 @@ export function UrlInputDialog({
{isExtracting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{dict.pdf.Extracting}
{dict.url.Extracting}
</>
) : (
<>
<Link className="mr-2 h-4 w-4" />
{dict.pdf.extract}
{dict.url.extract}
</>
)}
</Button>

View File

@@ -187,7 +187,7 @@
"chars": "chars",
"removeFile": "Remove file"
},
"pdf": {
"url": {
"title": "Extract Content from URL",
"description": "Paste a URL to extract and analyze its content",
"Extracting": "Extracting...",

View File

@@ -187,7 +187,7 @@
"chars": "文字",
"removeFile": "ファイルを削除"
},
"pdf": {
"url": {
"title": "URLからコンテンツを抽出",
"description": "URLを貼り付けてそのコンテンツを抽出および分析します",
"Extracting": "抽出中...",

View File

@@ -187,7 +187,7 @@
"chars": "字符",
"removeFile": "移除文件"
},
"pdf": {
"url": {
"title": "从 URL 提取内容",
"description": "粘贴 URL 以提取和分析其内容",
"Extracting": "提取中...",

View File

@@ -1,3 +1,5 @@
import { z } from "zod"
export interface UrlData {
url: string
title: string
@@ -6,6 +8,12 @@ export interface UrlData {
isExtracting: boolean
}
const UrlResponseSchema = z.object({
title: z.string().default("Untitled"),
content: z.string(),
charCount: z.number().int().nonnegative(),
})
export async function extractUrlContent(url: string): Promise<UrlData> {
const response = await fetch("/api/parse-url", {
method: "POST",
@@ -13,17 +21,29 @@ export async function extractUrlContent(url: string): Promise<UrlData> {
body: JSON.stringify({ url }),
})
// Try to parse JSON once
const raw = await response
.json()
.catch(() => ({ error: "Unexpected non-JSON response" }))
if (!response.ok) {
const error = await response.json().catch(() => null)
throw new Error(error?.error || "Failed to extract URL content")
const message =
typeof raw === "object" && raw && "error" in raw
? String((raw as any).error)
: "Failed to extract URL content"
throw new Error(message)
}
const parsed = UrlResponseSchema.safeParse(raw)
if (!parsed.success) {
throw new Error("Malformed response from URL extraction API")
}
const data = await response.json()
return {
url,
title: data.title,
content: data.content,
charCount: data.charCount,
title: parsed.data.title,
content: parsed.data.content,
charCount: parsed.data.charCount,
isExtracting: false,
}
}

1586
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,9 @@
"dist:mac": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac",
"dist:win": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --win",
"dist:linux": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --linux",
"dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac --win --linux"
"dist:all": "npm run electron:build && npm run electron:prepare && npx electron-builder --config electron/electron-builder.yml --mac --win --linux",
"test": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.1",
@@ -67,7 +69,6 @@
"cmdk": "^1.1.1",
"idb": "^8.0.3",
"js-tiktoken": "^1.0.21",
"jsdom": "^27.0.0",
"jsonrepair": "^3.13.1",
"lucide-react": "^0.562.0",
"motion": "^12.23.25",
@@ -105,14 +106,20 @@
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "^2.3.10",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/negotiator": "^0.6.4",
"@types/node": "^24.0.0",
"@types/pako": "^2.0.3",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"electron": "^39.2.7",
@@ -121,10 +128,13 @@
"eslint": "9.39.2",
"eslint-config-next": "16.1.1",
"husky": "^9.1.7",
"jsdom": "^27.4.0",
"lint-staged": "^16.2.7",
"shx": "^0.4.0",
"tailwindcss": "^4",
"typescript": "^5",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16",
"wait-on": "^9.0.3",
"wrangler": "4.54.0"
},