← Назад к вопросам

Как работает multipart/form-data?

2.0 Middle🔥 221 комментариев
#Dart#Работа с сетью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

multipart/form-data — это формат передачи данных в HTTP, используемый главным образом для загрузки файлов вместе с другими полями формы. Это один из трёх основных способов отправки данных через HTML формы.

Три типа кодирования данных формы

1. application/x-www-form-urlencoded (по умолчанию):

name=John&age=30&city=Moscow

Прост, но не подходит для файлов.

2. multipart/form-data (для файлов):

------WebKitFormBoundary123
Content-Disposition: form-data; name="name"

John
------WebKitFormBoundary123
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg

[бинарные данные файла]
------WebKitFormBoundary123--

3. text/plain (редко используется):

key1=value1
key2=value2

Структура multipart/form-data

Типичный запрос выглядит так:

POST /upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary123
Content-Length: 1234

------WebKitFormBoundary123
Content-Disposition: form-data; name="username"

john_doe
------WebKitFormBoundary123
Content-Disposition: form-data; name="email"

john@example.com
------WebKitFormBoundary123
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
Content-Transfer-Encoding: binary

[PNG image data]
------WebKitFormBoundary123
Content-Disposition: form-data; name="documents"; filename="resume.pdf"
Content-Type: application/pdf
Content-Transfer-Encoding: binary

[PDF document data]
------WebKitFormBoundary123--

Ключевые элементы

Boundary — строка-разделитель:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary123
                                            ^
                      Это уникальный разделитель для этого запроса

Headers для каждого поля:

Content-Disposition: form-data; name="fieldName"
Content-Type: image/jpeg (только для файлов)

Использование в Flutter с Dio

import 'package:dio/dio.dart';

class FileUploadService {
  final Dio _dio = Dio();

  Future<String> uploadFile(
    String filePath,
    String username,
    String email,
  ) async {
    try {
      // Создать FormData
      FormData formData = FormData.fromMap({
        'username': username,
        'email': email,
        'avatar': await MultipartFile.fromFile(
          filePath,
          filename: 'avatar.jpg',
          contentType: DioMediaType.parse('image/jpeg'),
        ),
      });

      // Отправить
      Response response = await _dio.post(
        'https://api.example.com/upload',
        data: formData,
      );

      return response.data['success'];
    } catch (e) {
      print('Upload failed: $e');
      rethrow;
    }
  }

  // Загрузить несколько файлов
  Future<void> uploadMultipleFiles(
    List<String> filePaths,
    Map<String, String> formFields,
  ) async {
    try {
      Map<String, dynamic> map = Map.from(formFields);

      for (int i = 0; i < filePaths.length; i++) {
        map['files'] = <MultipartFile>[
          for (String path in filePaths)
            await MultipartFile.fromFile(path),
        ];
      }

      FormData formData = FormData.fromMap(map);

      Response response = await _dio.post(
        'https://api.example.com/upload-multiple',
        data: formData,
      );

      print('Upload successful: ${response.data}');
    } catch (e) {
      print('Multi-file upload failed: $e');
      rethrow;
    }
  }
}

Использование в Flutter с http пакетом

import 'package:http/http.dart' as http;

class FileUploadHttp {
  Future<void> uploadFile(String filePath, String username) async {
    try {
      var request = http.MultipartRequest(
        'POST',
        Uri.parse('https://api.example.com/upload'),
      );

      // Добавить текстовые поля
      request.fields['username'] = username;
      request.fields['email'] = 'user@example.com';

      // Добавить файл
      request.files.add(
        await http.MultipartFile.fromPath(
          'avatar',
          filePath,
          contentType: MediaType('image', 'jpeg'),
        ),
      );

      // Отправить
      var response = await request.send();
      
      if (response.statusCode == 200) {
        print('Upload successful');
      } else {
        print('Upload failed: ${response.statusCode}');
      }
    } catch (e) {
      print('Error: $e');
    }
  }
}

Обработка на backend (пример с Express.js)

const express = require('express');
const multer = require('multer');
const app = express();

const upload = multer({ dest: 'uploads/' });

// Обработать multipart/form-data
app.post('/upload', upload.single('avatar'), (req, res) => {
  // req.file содержит информацию о файле
  // req.body содержит текстовые поля
  
  console.log('Username:', req.body.username);
  console.log('File:', req.file.filename);
  
  res.json({
    success: true,
    filename: req.file.filename,
  });
});

Обработка на backend (пример с FastAPI/Python)

from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post("/upload")
async def upload_file(
    username: str = Form(...),
    email: str = Form(...),
    avatar: UploadFile = File(...),
):
    # Сохранить файл
    contents = await avatar.read()
    with open(f"uploads/{avatar.filename}", "wb") as f:
        f.write(contents)
    
    return {
        "success": True,
        "username": username,
        "filename": avatar.filename,
    }

Отслеживание прогресса загрузки

import 'package:dio/dio.dart';

class ProgressiveUpload {
  final Dio _dio = Dio();

  Future<void> uploadWithProgress(
    String filePath,
    Function(int, int) onProgress,
  ) async {
    try {
      FormData formData = FormData.fromMap({
        'file': await MultipartFile.fromFile(filePath),
      });

      await _dio.post(
        'https://api.example.com/upload',
        data: formData,
        onSendProgress: (int sent, int total) {
          onProgress(sent, total);
          print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
        },
      );
    } catch (e) {
      print('Upload failed: $e');
    }
  }
}

Использование в UI

class FileUploadPage extends StatefulWidget {
  @override
  State<FileUploadPage> createState() => _FileUploadPageState();
}

class _FileUploadPageState extends State<FileUploadPage> {
  final _uploadService = FileUploadService();
  int _uploadProgress = 0;
  bool _isUploading = false;

  void _selectAndUpload() async {
    final result = await FilePicker.platform.pickFiles(type: FileType.image);

    if (result != null) {
      setState(() => _isUploading = true);

      try {
        await _uploadService.uploadWithProgress(
          result.files.single.path!,
          (sent, total) {
            setState(() => _uploadProgress = (sent / total * 100).toInt());
          },
        );
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Upload successful!')),
        );
      } catch (e) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Upload failed: $e')),
        );
      } finally {
        setState(() => _isUploading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('File Upload')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_isUploading)
              Column(
                children: [
                  LinearProgressIndicator(value: _uploadProgress / 100),
                  Text('$_uploadProgress%'),
                ],
              )
            else
              ElevatedButton(
                onPressed: _selectAndUpload,
                child: Text('Select and Upload File'),
              ),
          ],
        ),
      ),
    );
  }
}

Важные моменты

  • Boundary генерируется автоматически и должен быть уникален
  • Content-Type для файлов определяется автоматически (image/jpeg, application/pdf и т.д.)
  • Content-Length вычисляется HTTP клиентом
  • Порядок полей не имеет значения
  • Можно загружать несколько файлов в одном запросе
  • multipart/form-data неэффективен для большого количества маленьких полей — используйте JSON

multipart/form-data — это стандартный и надёжный способ для загрузки файлов в веб-приложениях.