chat proxy: multipart через files-only — data+files list ломает httpx
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,24 +62,31 @@ def _upstream_request_args(request, token: str) -> tuple[dict, HttpResponse | No
|
||||
return {'headers': headers}, None
|
||||
|
||||
if _is_multipart(request):
|
||||
data = []
|
||||
# list+list data/files в httpx ломает multipart (TypeError: tuple in join).
|
||||
# Текстовые поля тоже через files=(None, value).
|
||||
multipart = []
|
||||
total = 0
|
||||
for key in request.POST:
|
||||
for value in request.POST.getlist(key):
|
||||
data.append((key, value))
|
||||
files = []
|
||||
total = 0
|
||||
multipart.append((key, (None, str(value))))
|
||||
for key in request.FILES:
|
||||
for uploaded in request.FILES.getlist(key):
|
||||
total += uploaded.size
|
||||
if total > _MAX_BODY:
|
||||
return {}, HttpResponse(status=413)
|
||||
content = uploaded.read()
|
||||
files.append((
|
||||
if isinstance(content, str):
|
||||
content = content.encode('utf-8')
|
||||
multipart.append((
|
||||
key,
|
||||
(uploaded.name, content, uploaded.content_type or 'application/octet-stream'),
|
||||
(
|
||||
uploaded.name or 'file',
|
||||
content,
|
||||
uploaded.content_type or 'application/octet-stream',
|
||||
),
|
||||
))
|
||||
# Content-Type с boundary выставит httpx сам — исходный boundary уже «съеден».
|
||||
return {'headers': headers, 'data': data or None, 'files': files or None}, None
|
||||
# Content-Type с boundary выставит httpx сам.
|
||||
return {'headers': headers, 'files': multipart or None}, None
|
||||
|
||||
content_type = request.headers.get('Content-Type')
|
||||
if content_type:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from datetime import date
|
||||
from unittest import mock
|
||||
|
||||
import httpx
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from mattermostautodriver.exceptions import ResourceNotFound
|
||||
@@ -456,8 +457,13 @@ class ChatApiProxyTests(TestCase):
|
||||
self.assertEqual(_args[0], 'POST')
|
||||
self.assertTrue(_args[1].endswith('/api/v4/files'))
|
||||
self.assertIn('files', kwargs)
|
||||
self.assertIn('data', kwargs)
|
||||
self.assertNotIn('data', kwargs)
|
||||
self.assertNotIn('content', kwargs)
|
||||
# httpx сам ставит multipart boundary
|
||||
self.assertNotIn('Content-Type', kwargs['headers'])
|
||||
self.assertEqual(kwargs['headers']['Authorization'], 'Bearer user-pat-token')
|
||||
# httpx реально умеет закодировать этот формат
|
||||
built = httpx.Request('POST', 'https://mm.test.local/api/v4/files', files=kwargs['files'])
|
||||
self.assertIn('multipart/form-data', built.headers['content-type'])
|
||||
body = built.read()
|
||||
self.assertIn(b'channel_id', body)
|
||||
self.assertIn(b'hello', body)
|
||||
|
||||
Reference in New Issue
Block a user