mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
chore: drop old client support (#14369)
This commit is contained in:
@@ -105,10 +105,6 @@ class RemoveContextDocInput {
|
||||
class AddContextFileInput {
|
||||
@Field(() => String)
|
||||
contextId!: string;
|
||||
|
||||
// @TODO(@darkskygit): remove this after client lower then 0.22 has been disconnected
|
||||
@Field(() => String, { nullable: true, deprecationReason: 'Never used' })
|
||||
blobId!: string | undefined;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -1672,42 +1672,12 @@ const imageActions: Prompt[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// TODO(@darkskygit): deprecated, remove it after <0.22 version is outdated
|
||||
{
|
||||
name: 'debug:action:fal-remove-bg',
|
||||
action: 'Remove background',
|
||||
model: 'imageutils/rembg',
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
name: 'debug:action:fal-face-to-sticker',
|
||||
action: 'Convert to sticker',
|
||||
model: 'face-to-sticker',
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
name: 'debug:action:fal-teed',
|
||||
action: 'fal-teed',
|
||||
model: 'workflowutils/teed',
|
||||
messages: [{ role: 'user', content: '{{content}}' }],
|
||||
},
|
||||
{
|
||||
name: 'debug:action:fal-sd15',
|
||||
action: 'image',
|
||||
model: 'lcm-sd15-i2i',
|
||||
messages: [],
|
||||
},
|
||||
{
|
||||
name: 'debug:action:fal-upscaler',
|
||||
action: 'Clearer',
|
||||
model: 'clarity-upscaler',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'best quality, 8K resolution, highres, clarity, {{content}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const modelActions: Prompt[] = [
|
||||
|
||||
@@ -24,7 +24,9 @@ import {
|
||||
CopilotPromptInvalid,
|
||||
CopilotProviderNotSupported,
|
||||
CopilotProviderSideError,
|
||||
fetchBuffer,
|
||||
metrics,
|
||||
OneMB,
|
||||
UserFriendlyError,
|
||||
} from '../../../base';
|
||||
import { CopilotProvider } from './provider';
|
||||
@@ -673,14 +675,12 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
|
||||
for (const [idx, entry] of attachments.entries()) {
|
||||
const url = typeof entry === 'string' ? entry : entry.attachment;
|
||||
const resp = await fetch(url);
|
||||
if (resp.ok) {
|
||||
const type = resp.headers.get('content-type');
|
||||
if (type && type.startsWith('image/')) {
|
||||
const buffer = new Uint8Array(await resp.arrayBuffer());
|
||||
const file = new File([buffer], `${idx}.png`, { type });
|
||||
form.append('image[]', file);
|
||||
}
|
||||
try {
|
||||
const { buffer, type } = await fetchBuffer(url, 10 * OneMB, 'image/');
|
||||
const file = new File([buffer], `${idx}.png`, { type });
|
||||
form.append('image[]', file);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,22 @@ import {
|
||||
import { GoogleAuth, GoogleAuthOptions } from 'google-auth-library';
|
||||
import z, { ZodType } from 'zod';
|
||||
|
||||
import {
|
||||
bufferToArrayBuffer,
|
||||
fetchBuffer,
|
||||
OneMinute,
|
||||
ResponseTooLargeError,
|
||||
safeFetch,
|
||||
SsrfBlockedError,
|
||||
} from '../../../base';
|
||||
import { CustomAITools } from '../tools';
|
||||
import { PromptMessage, StreamObject } from './types';
|
||||
|
||||
type ChatMessage = CoreUserMessage | CoreAssistantMessage;
|
||||
|
||||
const ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
|
||||
const ATTACH_HEAD_PARAMS = { timeoutMs: OneMinute / 12, maxRedirects: 3 };
|
||||
|
||||
const SIMPLE_IMAGE_URL_REGEX = /^(https?:\/\/|data:image\/)/;
|
||||
const FORMAT_INFER_MAP: Record<string, string> = {
|
||||
pdf: 'application/pdf',
|
||||
@@ -42,6 +53,11 @@ const FORMAT_INFER_MAP: Record<string, string> = {
|
||||
flv: 'video/flv',
|
||||
};
|
||||
|
||||
async function fetchArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||
const { buffer } = await fetchBuffer(url, ATTACHMENT_MAX_BYTES);
|
||||
return bufferToArrayBuffer(buffer);
|
||||
}
|
||||
|
||||
export async function inferMimeType(url: string) {
|
||||
if (url.startsWith('data:')) {
|
||||
return url.split(';')[0].split(':')[1];
|
||||
@@ -53,12 +69,15 @@ export async function inferMimeType(url: string) {
|
||||
if (ext) {
|
||||
return ext;
|
||||
}
|
||||
const mimeType = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
redirect: 'follow',
|
||||
}).then(res => res.headers.get('Content-Type'));
|
||||
if (mimeType) {
|
||||
return mimeType;
|
||||
try {
|
||||
const mimeType = await safeFetch(
|
||||
url,
|
||||
{ method: 'HEAD' },
|
||||
ATTACH_HEAD_PARAMS
|
||||
).then(res => res.headers.get('content-type'));
|
||||
if (mimeType) return mimeType;
|
||||
} catch {
|
||||
// ignore and fallback to default
|
||||
}
|
||||
}
|
||||
return 'application/octet-stream';
|
||||
@@ -106,7 +125,16 @@ export async function chatToGPTMessage(
|
||||
if (SIMPLE_IMAGE_URL_REGEX.test(attachment)) {
|
||||
const data =
|
||||
attachment.startsWith('data:') || useBase64Attachment
|
||||
? await fetch(attachment).then(r => r.arrayBuffer())
|
||||
? await fetchArrayBuffer(attachment).catch(error => {
|
||||
// Avoid leaking internal details for blocked URLs.
|
||||
if (
|
||||
error instanceof SsrfBlockedError ||
|
||||
error instanceof ResponseTooLargeError
|
||||
) {
|
||||
throw new Error('Attachment URL is not allowed');
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
: new URL(attachment);
|
||||
if (mediaType.startsWith('image/')) {
|
||||
contents.push({ type: 'image', image: data, mediaType });
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
BlobQuotaExceeded,
|
||||
CallMetric,
|
||||
Config,
|
||||
fetchBuffer,
|
||||
type FileUpload,
|
||||
OneMB,
|
||||
OnEvent,
|
||||
readBuffer,
|
||||
type StorageProvider,
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
} from '../../base';
|
||||
import { QuotaService } from '../../core/quota';
|
||||
|
||||
const REMOTE_BLOB_MAX_BYTES = 20 * OneMB;
|
||||
|
||||
@Injectable()
|
||||
export class CopilotStorage {
|
||||
public provider!: StorageProvider;
|
||||
@@ -88,9 +92,8 @@ export class CopilotStorage {
|
||||
|
||||
@CallMetric('ai', 'blob_proxy_remote_url')
|
||||
async handleRemoteLink(userId: string, workspaceId: string, link: string) {
|
||||
const response = await fetch(link);
|
||||
const buffer = new Uint8Array(await response.arrayBuffer());
|
||||
const { buffer } = await fetchBuffer(link, REMOTE_BLOB_MAX_BYTES, 'image/');
|
||||
const filename = createHash('sha256').update(buffer).digest('base64url');
|
||||
return this.put(userId, workspaceId, filename, Buffer.from(buffer));
|
||||
return this.put(userId, workspaceId, filename, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ConnectedAccount } from '@prisma/client';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import {
|
||||
ActionForbidden,
|
||||
Config,
|
||||
InvalidAuthState,
|
||||
InvalidOauthCallbackState,
|
||||
@@ -57,6 +58,9 @@ export class OAuthController {
|
||||
if (!unknownProviderName) {
|
||||
throw new MissingOauthQueryParameter({ name: 'provider' });
|
||||
}
|
||||
if (!clientNonce) {
|
||||
throw new MissingOauthQueryParameter({ name: 'client_nonce' });
|
||||
}
|
||||
|
||||
const providerName = OAuthProviderName[unknownProviderName];
|
||||
const provider = this.providerFactory.get(providerName);
|
||||
@@ -67,6 +71,10 @@ export class OAuthController {
|
||||
|
||||
const pkce = provider.requiresPkce ? this.oauth.createPkcePair() : null;
|
||||
|
||||
if (redirectUri && !this.url.isAllowedRedirectUri(redirectUri)) {
|
||||
throw new ActionForbidden();
|
||||
}
|
||||
|
||||
const state = await this.oauth.saveOAuthState({
|
||||
provider: providerName,
|
||||
redirectUri,
|
||||
@@ -173,16 +181,6 @@ export class OAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(@fengmk2): clientNonce should be required after the client version >= 0.21.0
|
||||
if (
|
||||
state.clientNonce &&
|
||||
state.clientNonce !== clientNonce &&
|
||||
// apple sign in with nonce stored in id token
|
||||
state.provider !== OAuthProviderName.Apple
|
||||
) {
|
||||
throw new InvalidAuthState();
|
||||
}
|
||||
|
||||
if (!state.provider) {
|
||||
throw new MissingOauthQueryParameter({ name: 'provider' });
|
||||
}
|
||||
@@ -193,6 +191,13 @@ export class OAuthController {
|
||||
throw new UnknownOauthProvider({ name: state.provider ?? 'unknown' });
|
||||
}
|
||||
|
||||
if (
|
||||
state.provider !== OAuthProviderName.Apple &&
|
||||
(!clientNonce || !state.clientNonce || state.clientNonce !== clientNonce)
|
||||
) {
|
||||
throw new InvalidAuthState();
|
||||
}
|
||||
|
||||
let tokens: Tokens;
|
||||
try {
|
||||
tokens = await provider.getToken(code, state);
|
||||
@@ -221,7 +226,7 @@ export class OAuthController {
|
||||
state.provider === OAuthProviderName.Apple &&
|
||||
(!state.client || state.client === 'web')
|
||||
) {
|
||||
return res.redirect(this.url.link(state.redirectUri ?? '/'));
|
||||
return this.url.safeRedirect(res, state.redirectUri ?? '/');
|
||||
}
|
||||
|
||||
res.send({
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
import {
|
||||
Context,
|
||||
registerEnumType,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { Request } from 'express';
|
||||
import semver from 'semver';
|
||||
import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { getClientVersionFromRequest } from '../../base';
|
||||
import { ServerConfigType } from '../../core/config/types';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
|
||||
registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
|
||||
|
||||
const APPLE_OAUTH_PROVIDER_MIN_VERSION = new semver.Range('>=0.22.0', {
|
||||
includePrerelease: true,
|
||||
});
|
||||
|
||||
@Resolver(() => ServerConfigType)
|
||||
export class OAuthResolver {
|
||||
constructor(private readonly factory: OAuthProviderFactory) {}
|
||||
|
||||
@ResolveField(() => [OAuthProviderName])
|
||||
oauthProviders(@Context() ctx: { req: Request }) {
|
||||
// Apple oauth provider is not supported in client version < 0.22.0
|
||||
const providers = this.factory.providers;
|
||||
if (providers.includes(OAuthProviderName.Apple)) {
|
||||
const version = getClientVersionFromRequest(ctx.req);
|
||||
if (!version || !APPLE_OAUTH_PROVIDER_MIN_VERSION.test(version)) {
|
||||
return providers.filter(p => p !== OAuthProviderName.Apple);
|
||||
}
|
||||
}
|
||||
|
||||
return providers;
|
||||
oauthProviders() {
|
||||
return this.factory.providers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,23 @@ import {
|
||||
Req,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import type {
|
||||
Request as ExpressRequest,
|
||||
Response as ExpressResponse,
|
||||
} from 'express';
|
||||
import { HTMLRewriter } from 'htmlrewriter';
|
||||
|
||||
import { BadRequest, Cache, URLHelper, UseNamedGuard } from '../../base';
|
||||
import {
|
||||
BadRequest,
|
||||
Cache,
|
||||
readResponseBufferWithLimit,
|
||||
ResponseTooLargeError,
|
||||
safeFetch,
|
||||
SsrfBlockedError,
|
||||
type SSRFBlockReason,
|
||||
URLHelper,
|
||||
UseNamedGuard,
|
||||
} from '../../base';
|
||||
import { Public } from '../../core/auth';
|
||||
import { WorkerService } from './service';
|
||||
import type { LinkPreviewRequest, LinkPreviewResponse } from './types';
|
||||
@@ -28,6 +41,25 @@ import { decodeWithCharset } from './utils/encoding';
|
||||
|
||||
// cache for 30 minutes
|
||||
const CACHE_TTL = 1000 * 60 * 30;
|
||||
const MAX_REDIRECTS = 3;
|
||||
const FETCH_TIMEOUT_MS = 10_000;
|
||||
const IMAGE_PROXY_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const LINK_PREVIEW_MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
function toBadRequestReason(reason: SSRFBlockReason) {
|
||||
switch (reason) {
|
||||
case 'disallowed_protocol':
|
||||
case 'url_has_credentials':
|
||||
case 'blocked_hostname':
|
||||
case 'blocked_ip':
|
||||
case 'invalid_url':
|
||||
return 'Invalid URL';
|
||||
case 'unresolvable_hostname':
|
||||
return 'Failed to resolve hostname';
|
||||
case 'too_many_redirects':
|
||||
return 'Too many redirects';
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@UseNamedGuard('selfhost')
|
||||
@@ -45,14 +77,33 @@ export class WorkerController {
|
||||
return this.service.allowedOrigins;
|
||||
}
|
||||
|
||||
@Options('/image-proxy')
|
||||
imageProxyOption(
|
||||
@Req() request: ExpressRequest,
|
||||
@Res() resp: ExpressResponse
|
||||
) {
|
||||
const origin = request.headers.origin;
|
||||
return resp
|
||||
.status(204)
|
||||
.header({
|
||||
...getCorsHeaders(origin),
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
})
|
||||
.send();
|
||||
}
|
||||
|
||||
@Get('/image-proxy')
|
||||
async imageProxy(@Req() req: Request, @Res() resp: Response) {
|
||||
const origin = req.headers.origin ?? '';
|
||||
async imageProxy(@Req() req: ExpressRequest, @Res() resp: ExpressResponse) {
|
||||
const origin = req.headers.origin;
|
||||
const referer = req.headers.referer;
|
||||
if (
|
||||
(origin && !isOriginAllowed(origin, this.allowedOrigin)) ||
|
||||
(referer && !isRefererAllowed(referer, this.allowedOrigin))
|
||||
) {
|
||||
const originAllowed = origin
|
||||
? isOriginAllowed(origin, this.allowedOrigin)
|
||||
: false;
|
||||
const refererAllowed = referer
|
||||
? isRefererAllowed(referer, this.allowedOrigin)
|
||||
: false;
|
||||
if (!originAllowed && !refererAllowed) {
|
||||
this.logger.error('Invalid Origin', 'ERROR', { origin, referer });
|
||||
throw new BadRequest('Invalid header');
|
||||
}
|
||||
@@ -79,24 +130,66 @@ export class WorkerController {
|
||||
return resp
|
||||
.status(200)
|
||||
.header({
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
Vary: 'Origin',
|
||||
...getCorsHeaders(origin),
|
||||
...(origin ? { Vary: 'Origin' } : {}),
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Content-Type': 'image/*',
|
||||
})
|
||||
.send(buffer);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
new Request(targetURL.toString(), {
|
||||
method: 'GET',
|
||||
headers: cloneHeader(req.headers),
|
||||
})
|
||||
);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await safeFetch(
|
||||
targetURL.toString(),
|
||||
{ method: 'GET', headers: cloneHeader(req.headers) },
|
||||
{ timeoutMs: FETCH_TIMEOUT_MS, maxRedirects: MAX_REDIRECTS }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof SsrfBlockedError) {
|
||||
const reason = error.data?.reason as SSRFBlockReason | undefined;
|
||||
this.logger.warn('Blocked image proxy target', {
|
||||
url: imageURL,
|
||||
reason,
|
||||
context: (error as any).context,
|
||||
});
|
||||
throw new BadRequest(toBadRequestReason(reason ?? 'invalid_url'));
|
||||
}
|
||||
if (error instanceof ResponseTooLargeError) {
|
||||
this.logger.warn('Image proxy response too large', {
|
||||
url: imageURL,
|
||||
limitBytes: error.data?.limitBytes,
|
||||
receivedBytes: error.data?.receivedBytes,
|
||||
});
|
||||
throw new BadRequest('Response too large');
|
||||
}
|
||||
this.logger.error('Failed to fetch image', {
|
||||
origin,
|
||||
url: imageURL,
|
||||
error,
|
||||
});
|
||||
throw new BadRequest('Failed to fetch image');
|
||||
}
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType?.startsWith('image/')) {
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await readResponseBufferWithLimit(
|
||||
response,
|
||||
IMAGE_PROXY_MAX_BYTES
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ResponseTooLargeError) {
|
||||
this.logger.warn('Image proxy response too large', {
|
||||
url: imageURL,
|
||||
limitBytes: error.data?.limitBytes,
|
||||
receivedBytes: error.data?.receivedBytes,
|
||||
});
|
||||
throw new BadRequest('Response too large');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.cache.set(cachedUrl, buffer.toString('base64'), {
|
||||
ttl: CACHE_TTL,
|
||||
});
|
||||
@@ -104,8 +197,8 @@ export class WorkerController {
|
||||
return resp
|
||||
.status(200)
|
||||
.header({
|
||||
'Access-Control-Allow-Origin': origin ?? 'null',
|
||||
Vary: 'Origin',
|
||||
...getCorsHeaders(origin),
|
||||
...(origin ? { Vary: 'Origin' } : {}),
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': contentDisposition,
|
||||
@@ -124,17 +217,20 @@ export class WorkerController {
|
||||
this.logger.error('Failed to fetch image', {
|
||||
origin,
|
||||
url: imageURL,
|
||||
status: resp.status,
|
||||
status: response.status,
|
||||
});
|
||||
throw new BadRequest('Failed to fetch image');
|
||||
}
|
||||
}
|
||||
|
||||
@Options('/link-preview')
|
||||
linkPreviewOption(@Req() request: Request, @Res() resp: Response) {
|
||||
linkPreviewOption(
|
||||
@Req() request: ExpressRequest,
|
||||
@Res() resp: ExpressResponse
|
||||
) {
|
||||
const origin = request.headers.origin;
|
||||
return resp
|
||||
.status(200)
|
||||
.status(204)
|
||||
.header({
|
||||
...getCorsHeaders(origin),
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
@@ -145,15 +241,18 @@ export class WorkerController {
|
||||
|
||||
@Post('/link-preview')
|
||||
async linkPreview(
|
||||
@Req() request: Request,
|
||||
@Res() resp: Response
|
||||
): Promise<Response> {
|
||||
@Req() request: ExpressRequest,
|
||||
@Res() resp: ExpressResponse
|
||||
): Promise<ExpressResponse> {
|
||||
const origin = request.headers.origin;
|
||||
const referer = request.headers.referer;
|
||||
if (
|
||||
(origin && !isOriginAllowed(origin, this.allowedOrigin)) ||
|
||||
(referer && !isRefererAllowed(referer, this.allowedOrigin))
|
||||
) {
|
||||
const originAllowed = origin
|
||||
? isOriginAllowed(origin, this.allowedOrigin)
|
||||
: false;
|
||||
const refererAllowed = referer
|
||||
? isRefererAllowed(referer, this.allowedOrigin)
|
||||
: false;
|
||||
if (!originAllowed && !refererAllowed) {
|
||||
this.logger.error('Invalid Origin', { origin, referer });
|
||||
throw new BadRequest('Invalid header');
|
||||
}
|
||||
@@ -183,9 +282,13 @@ export class WorkerController {
|
||||
.send(cachedResponse);
|
||||
}
|
||||
|
||||
const response = await fetch(targetURL, {
|
||||
headers: cloneHeader(request.headers),
|
||||
});
|
||||
const method: 'GET' | 'HEAD' = requestBody?.head ? 'HEAD' : 'GET';
|
||||
|
||||
const response = await safeFetch(
|
||||
targetURL.toString(),
|
||||
{ method, headers: cloneHeader(request.headers) },
|
||||
{ timeoutMs: FETCH_TIMEOUT_MS, maxRedirects: MAX_REDIRECTS }
|
||||
);
|
||||
this.logger.debug('Fetched URL', {
|
||||
origin,
|
||||
url: targetURL,
|
||||
@@ -211,7 +314,12 @@ export class WorkerController {
|
||||
};
|
||||
|
||||
if (response.body) {
|
||||
const resp = await decodeWithCharset(response, res);
|
||||
const body = await readResponseBufferWithLimit(
|
||||
response,
|
||||
LINK_PREVIEW_MAX_BYTES
|
||||
);
|
||||
const limitedResponse = new Response(body, response);
|
||||
const resp = await decodeWithCharset(limitedResponse, res);
|
||||
|
||||
const rewriter = new HTMLRewriter()
|
||||
.on('meta', {
|
||||
@@ -287,7 +395,11 @@ export class WorkerController {
|
||||
{
|
||||
// head default path of favicon
|
||||
const faviconUrl = new URL('/favicon.ico?v=2', response.url);
|
||||
const faviconResponse = await fetch(faviconUrl, { method: 'HEAD' });
|
||||
const faviconResponse = await safeFetch(
|
||||
faviconUrl.toString(),
|
||||
{ method: 'HEAD' },
|
||||
{ timeoutMs: FETCH_TIMEOUT_MS, maxRedirects: MAX_REDIRECTS }
|
||||
);
|
||||
if (faviconResponse.ok) {
|
||||
appendUrl(faviconUrl.toString(), res.favicons);
|
||||
}
|
||||
@@ -311,6 +423,25 @@ export class WorkerController {
|
||||
})
|
||||
.send(json);
|
||||
} catch (error) {
|
||||
if (error instanceof SsrfBlockedError) {
|
||||
const reason = error.data?.reason as SSRFBlockReason | undefined;
|
||||
this.logger.warn('Blocked link preview target', {
|
||||
origin,
|
||||
url: requestBody?.url,
|
||||
reason,
|
||||
context: (error as any).context,
|
||||
});
|
||||
throw new BadRequest(toBadRequestReason(reason ?? 'invalid_url'));
|
||||
}
|
||||
if (error instanceof ResponseTooLargeError) {
|
||||
this.logger.warn('Link preview response too large', {
|
||||
origin,
|
||||
url: requestBody?.url,
|
||||
limitBytes: error.data?.limitBytes,
|
||||
receivedBytes: error.data?.receivedBytes,
|
||||
});
|
||||
throw new BadRequest('Response too large');
|
||||
}
|
||||
this.logger.error('Error fetching URL', {
|
||||
origin,
|
||||
url: targetURL,
|
||||
|
||||
Reference in New Issue
Block a user