feat: proxy GetObject from Telegram CDN for real S3 client compatibility

- Add proxyS3Get config (PROXY_S3_GET env, default true) to env.ts
- Proxy handleGetObject and handleGetMultipartObject: fetch from Telegram
  CDN and return 200 with streaming body instead of 302 redirect
- Real S3 clients (AWS SDK v3) expect 200+body on GetObject, not redirect
- Legacy 302 redirect path preserved when proxyS3Get=false
- Updated production-e2e: GetObject asserts 200 with body content
- Updated s3-sdk.test.ts: GetObject asserts 200 with body (removed try/catch)
- Cleaned up unused multipart imports in s3-sdk.test.ts

All 49 tests pass (29 production-e2e + 20 s3-sdk).
This commit is contained in:
asepharyana
2026-07-07 02:54:51 +07:00
parent f88ac1a124
commit 06f93e30f6
4 changed files with 94 additions and 32 deletions
+13 -6
View File
@@ -236,10 +236,12 @@ describe('S3 API (production, SigV4)', () => {
expect(Number(r.headers.get('content-length'))).toBeGreaterThan(0);
});
it('GetObject (GET /{bucket}/{key}) — redirects to Telegram', async () => {
it('GetObject (GET /{bucket}/{key}) — proxies content from Telegram', async () => {
const r = await s3Request('GET', `/${bucketName}/test-file.txt`);
expect([200, 302]).toContain(r.status);
if (r.status === 302) expect(r.headers.get('location')).toBeTruthy();
expect(r.status).toBe(200);
const text = await r.text();
expect(text).toContain('hello s3');
expect(r.headers.get('content-type')).toMatch(/text|octet/);
});
it('ListObjectsV1 (GET /{bucket})', async () => {
@@ -337,12 +339,17 @@ describe('S3 API (production, SigV4)', () => {
const presignedUrl = `${BASE_URL}/${bucketName}/presigned-test.txt?${sp.toString()}`;
const r = await fetch(presignedUrl);
// Presigned URL should return 302 (redirect to Telegram) or 403 (auth fail)
// Presigned URL: 200 (proxied body), 302 (redirect), or 403 (auth fail)
if (r.status === 403) {
console.warn('⚠️ Presigned URL returned 403 — verification mismatch');
}
expect([302, 403]).toContain(r.status);
if (r.status === 302) expect(r.headers.get('location')).toBeTruthy();
expect([200, 302, 403]).toContain(r.status);
if (r.status === 200) {
const text = await r.text();
expect(text).toContain('presigned content');
} else if (r.status === 302) {
expect(r.headers.get('location')).toBeTruthy();
}
});
it('Delete bucket — must be empty first', async () => {
+10 -20
View File
@@ -31,11 +31,8 @@ import {
CopyObjectCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
AbortMultipartUploadCommand,
ListMultipartUploadsCommand,
AbortMultipartUploadCommand,
NoSuchKey,
NotFound,
} from '@aws-sdk/client-s3';
@@ -158,22 +155,15 @@ describe('S3 SDK compatibility', () => {
expect(ContentType).toBe('text/plain');
});
it('GetObject returns redirect to Telegram (302)', async () => {
// TeleUploader returns 302 -> Telegram CDN rather than the raw object body.
// The AWS SDK follows the redirect but the target (api.telegram.org) returns
// non-XML, so the deserializer throws UnknownError.
// We accept either a 200 with body OR an error with 302 status.
try {
const { Body, ContentType } = await s3.send(
new GetObjectCommand({ Bucket: BUCKET, Key: 'hello-sdk.txt' }),
);
const text = await Body!.transformToString();
expect(text).toBe('Hello from AWS SDK v3!');
expect(ContentType).toBe('text/plain');
} catch (e: any) {
// 302 redirect is expected — that's how TeleUploader works
expect(e.$metadata?.httpStatusCode).toBe(302);
}
it('GetObject returns stored content (proxied from Telegram)', async () => {
const { Body, ContentType, ContentLength, ETag } = await s3.send(
new GetObjectCommand({ Bucket: BUCKET, Key: 'hello-sdk.txt' }),
);
const text = await Body!.transformToString();
expect(text).toBe('Hello from AWS SDK v3!');
expect(ContentType).toBe('text/plain');
expect(ContentLength).toBeGreaterThan(0);
expect(ETag).toBeTruthy();
});
it('GetObject returns 404 (NoSuchKey) for missing key', async () => {