Клиент видел имя файла на экране и уверен, что приложил смету, а в карточке заявки вложения нет. Между «файл выбран» и «файл принят вместе с заявкой» помещается вся цепочка передачи, проверки и привязки, и её нужно сделать видимой.
«Файл прикреплён» означает, что сервер принял байты, проверил содержимое, сохранил файл вне публичного доступа и связал его с конкретной заявкой по идентификатору. До этого браузер показывает лишь локальную ссылку на диск клиента. Поэтому загрузку отделяют от отправки формы, вводят статусы от «выбран» до «принят» и не активируют отправку, пока вложение не подтверждено сервером.
Заявка на расчёт уходит в CRM, менеджер открывает карточку, а вложения нет. Клиент уверен, что приложил смету, потому что видел имя файла на экране. Условная сцена: сумма договора 480 000 рублей, файл 12 МБ, интернет в офисе клиента периодически отваливается. Никто не врёт — просто «файл выбран» и «файл принят вместе с заявкой» это два разных состояния, между которыми помещается вся драма.
Механизм здесь простой и потому коварный. Браузер даёт веб-странице доступ к файлу через объект File, но это лишь ссылка на локальный файл на устройстве пользователя, а не копия на сервере. Пока не произошла передача байтов по сети и пока принимающая сторона не подтвердила, что сохранила их и связала с конкретной заявкой, файла в вашей системе не существует. Всё, что видит клиент в форме, — это его собственный диск, отражённый в интерфейсе. Разберём, как выстроить цепочку от выбора до подтверждённой передачи так, чтобы карточка заявки и реальность совпадали.

Что на самом деле означает «файл прикреплён»
В HTML-форме поле <input type="file"> даёт посетителю выбрать один или несколько файлов. Спецификация File API, которую описывает MDN, устроена так: страница получает объекты File с именем, размером и типом, может прочитать содержимое через FileReader или отправить его в составе FormData. Ни в один из этих моментов файл ещё не покинул компьютер клиента. Даже если вы показали превью и имя файла зелёным цветом, вы показали локальную ссылку.
Отсюда первое практическое следствие: в интерфейсе нельзя писать «файл прикреплён», пока сервер не вернул идентификатор вложения. Корректная формулировка на этапе выбора — «файл выбран», после отправки — «файл загружается», и только после ответа сервера — «файл принят». Три состояния, три разных слова. Это не педантизм: клиент, который видит «прикреплён» и закрывает вкладку, рассчитывает, что дело сделано.
Второе следствие касается того, что вы храните. Имя файла, размер и заявленный MIME-тип приходят от клиента и не являются доказательством содержимого. Расширение .pdf ничего не гарантирует: под ним может лежать что угодно. Поэтому проверка типа — задача принимающей стороны, а не браузера, и делать её нужно по фактическому содержимому, а не по строке, которую прислал клиент.
Если ваша задача — проверить передачу файлов из формы, на странице соответствующего направления 404ai можно посмотреть опубликованные возможности и оставить описание текущего процесса. Конкретные подключения, исходные данные и критерии приёмки согласуем отдельно.
Загрузка как отдельный шаг, а не побочный эффект отправки формы
Классическая ошибка — отправлять файл тем же запросом, что и текстовые поля заявки, одним пакетом. Тогда любая проблема с файлом (обрыв, слишком большой размер, отклонённый тип) ломает всю заявку: клиент заполнил десять полей, а получает ошибку и теряет введённое. Разделение на два действия — сначала загрузить файл, получить его идентификатор, потом отправить заявку с этим идентификатором — снимает связку.
Такой подход перекликается с паттерном асинхронного запрос-ответа, который Microsoft описывает для длительных операций: клиент инициирует работу, получает подтверждение приёма и отдельно узнаёт о результате. Для вложений это означает, что загрузка может идти в фоне, а заявка уходит, как только файл получил статус «принят». Если файл ещё в процессе, заявку либо не отправляют, либо отправляют с явной пометкой «вложение в обработке» — но не молча.
Тут важно не переусердствовать. Раздельная загрузка добавляет состояний, и если их не показать, клиент запутается сильнее, чем при одной кнопке. Поэтому интерфейс обязан честно отображать, на каком шаге находится каждый файл. Об этом ниже, в разделе про экран сотрудника и клиента.
Эпизод первый: обрыв связи на середине передачи
Условный сценарий. Клиент в регионе с нестабильным мобильным интернетом прикладывает сканированный договор на 8 МБ. Загрузка доходит до 60%, сеть пропадает на сорок секунд, браузер показывает ошибку. Клиент нажимает «Отправить» ещё раз. Что происходит дальше — зависит от того, как устроена ваша форма.
Если загрузка не поддерживает возобновление и повтор, второй клик начинает всё с нуля, а от первого недописанного куска на сервере может остаться частичный приём — в зависимости от того, как сервер обрабатывает обрыв. Если сервер не связал частичную загрузку с сессией, вы получаете два осиротевших фрагмента и ни одного целого файла. Если же форма при повторной отправке создаёт новую заявку, у вас две заявки и ноль вложений.
Правильная реакция на обрыв — не «попробуйте ещё раз», а сохранение прогресса и понятный статус. Технически это может быть возобновляемая загрузка по частям (chunked upload) с идентификатором сессии, чтобы после восстановления связи продолжить с места разрыва. Но даже без возобновления форма обязана: сохранить уже введённые текстовые поля, показать, что именно не удалось, и не создавать дубль заявки при повторной попытке.
Отдельно про повтор отправки. Идемпотентность здесь не роскошь, а необходимость: у заявки должен быть клиентский ключ (например, сгенерированный при открытии формы), и повторная отправка с тем же ключом не плодит новые записи, а обновляет существующую. Это защищает и от двойного клика, и от повторной попытки после таймаута, когда клиент не знает, дошло ли.
Эпизод второй: тип не тот, размер великоват, а проверка ещё не закончилась
Условный сценарий. Клиент прикладывает файл смета.xlsx, но по факту это архив с расширением таблицы. Или прикладывает фото с телефона на 22 МБ, когда форма рассчитана на 10 МБ. Или прикладывает корректный PDF, но антивирусная проверка на сервере занимает несколько секунд, и в этот момент клиент уже нажал «Отправить».
Три разные проблемы, три разных ответа. По типу: браузерная проверка по расширению — это подсказка, а не защита. Реальную проверку делает сервер, и делать её нужно по сигнатуре содержимого, а не по присланному MIME. OWASP в шпаргалке по загрузке файлов прямо предупреждает: нельзя доверять имени и типу от клиента, нужно ограничивать допустимые типы по белому списку, проверять содержимое, переименовывать сохранённые файлы и хранить их вне корня веб-приложения. Это не паранойя, а базовая гигиена.
По размеру: лимит должен быть известен заранее, а не всплывать после пяти минут загрузки. Если заявленный размер файла больше допустимого, отклоните его до начала передачи; фактический размер сервер всё равно проверяет при приёме. И дайте клиенту понятную альтернативу — сжать, разбить, отправить ссылку, — иначе он уйдёт искать другой канал.
По незавершённой проверке: пока файл не прошёл проверку, он не должен считаться принятым. Заявка в этот момент может существовать, но со статусом вложения «на проверке». Менеджер, открывший карточку, видит честную картину, а не пустоту, которую он примет за отсутствие файла.
Файл, связанный с заявкой: что именно вы передаёте
Прежде чем строить интерфейс, стоит договориться, что такое «вложение» в вашей системе. Это не просто файл на диске. Это запись со своей жизнью. Условная форма такого объекта:
| Поле | Смысл | Кто заполняет |
|---|---|---|
attachment_id |
Устойчивый идентификатор вложения | Сервер при приёме |
request_id |
К какой заявке привязано | Сервер после отправки заявки |
original_name |
Имя, как его видел клиент | Клиент (справочно) |
size_bytes |
Фактический размер | Сервер после приёма |
detected_type |
Тип по содержимому | Сервер при проверке |
status |
uploading / received / checking / accepted / rejected |
Сервер |
checksum |
Контрольная сумма для сверки целостности | Сервер |
storage_ref |
Где физически лежит | Сервер, не клиент |
reject_reason |
Почему отклонён | Сервер |
Ключевое здесь — status и request_id. Пока request_id пуст, вложение ничьё: оно загружено, но не привязано к заявке. Пока status не accepted, вложение не считается частью принятой заявки. Эти два поля закрывают большинство споров «я же прикладывал».
Контрольная сумма решает ещё одну проблему — целостность. Если файл дошёл, но побился при передаче, сверка суммы покажет расхождение, и вы не отдадите менеджеру битый документ под видом рабочего.
Как это выглядит на экране сотрудника и клиента
У клиента и у менеджера разные задачи, поэтому и экраны разные. Клиенту нужно понимать, что происходит с его файлом прямо сейчас. Менеджеру — видеть, что во вложении, можно ли на него опираться и кто его прислал.
Для клиента достаточно строки на каждый файл с четырьмя состояниями: «выбран», «загружается (с прогрессом)», «проверяется», «принят» или «отклонён с причиной». Кнопка «Отправить» активна, только когда все файлы в состоянии «принят» или когда клиент явно отказался от вложения. Никаких зелёных галочек раньше времени.
Для менеджера в карточке заявки — компактный блок вложений: имя, размер, тип, статус, кто и когда загрузил, кнопка скачать. Если файл отклонён, менеджер видит причину и может попросить клиента прислать заново, не гадая. Если файл на проверке — видит это и не звонит клиенту с вопросом «а где смета».
Отдельный вопрос — доступ. Вложение должно открываться только тем, кому положено по роли и по принадлежности к заявке. Прямая ссылка на файл, которую можно переслать коллеге из другого отдела или случайно опубликовать, — это утечка. Поэтому скачивание идёт через проверку прав, а не по угадываемому адресу. OWASP отдельно напоминает: храните загруженное вне публичного корня и отдавайте через контролируемый обработчик.
Файл, связанный с заявкой: исключения и проверка условий
Любая схема с состояниями живёт ровно до первого исключения. Перечислим те, что встречаются чаще всего, и что с ними делать.
Файл загружен, заявка не отправлена. Клиент приложил файл, получил «принят», но закрыл вкладку до отправки заявки. Вложение висит без request_id. Нужна политика очистки: такие файлы живут ограниченное время и удаляются, если к ним не привязалась заявка. Иначе хранилище тихо зарастает сиротами.
Заявка отправлена дважды. Клиент нажал «Отправить», не увидел реакции, нажал ещё раз. Без ключа идемпотентности — две заявки и два вложения, причём второе может быть копией первого. С ключом — одна заявка, обновлённая повторным запросом.
Файл отклонён, но клиент этого не увидел. Проверка завершилась уже после того, как клиент закрыл страницу. Он уверен, что всё хорошо. Здесь помогает уведомление на почту или в личный кабинет со статусом вложения, а не молчаливое отклонение.
Один и тот же файл приложен к разным заявкам. Иногда это нормально (типовой договор), иногда — ошибка клиента. Решение зависит от того, храните ли вы файлы дедуплицированно по контрольной сумме или каждое вложение независимо. Оба варианта рабочие, но выбор надо сделать осознанно, а не по инерции.
Клиент приложил секреты. Иногда в поле для сметы попадает файл с доступами, клиентской базой или сканом паспорта. Это уже вопрос не техники, а политики: что вы принимаете через публичную форму, а что — только по защищённому каналу. Демо и тестовые формы не должны принимать такие данные вовсе, а для боевых нужна понятная инструкция клиенту и проверка у уполномоченного сотрудника по применимым документам компании.
Проверка типа: почему расширение ничего не доказывает
Вернёмся к проверке содержимого, потому что это место, где чаще всего экономят. Браузер сообщает MIME-тип, но он вычисляется по расширению и легко подделывается. Сервер, который доверяет этому полю, фактически доверяет клиенту решать, что за файл он прислал.
Разумный минимум: белый список допустимых типов, проверка по сигнатуре (магическим байтам) в начале файла, ограничение размера, переименование при сохранении (имя клиента — только метаданные, не путь на диске), хранение вне публичной директории, отдача через контролируемый обработчик с проверкой прав. OWASP перечисляет это как базовый набор, и он же закрывает большинство сценариев злоупотребления.
Отдельно про исполняемые форматы и архивы. Если бизнес-задача не требует принимать .exe, .js, .html или архивы, не принимайте их. Каждый лишний разрешённый тип — это лишняя поверхность. А если архив нужен, помните, что внутри может быть что угодно, и распаковывать его на сервере без ограничений — плохая идея.
Как проверить, что передача действительно работает
Проверка — это не «загрузил один файл, всё ок». Нужен набор сценариев, которые прогоняются до запуска и после каждого изменения формы. Условный чек-лист:
| Сценарий | Что проверяем | Ожидаемый результат |
|---|---|---|
| Обычный файл допустимого типа | Приём и привязка | Вложение в карточке, статус accepted |
| Файл больше лимита | Отклонение до передачи | Понятная ошибка, заявка не ломается |
| Файл с подменённым расширением | Проверка по содержимому | Отклонение, причина видна |
| Обрыв связи на середине | Повтор и возобновление | Нет дублей, прогресс сохраняется |
| Двойной клик по «Отправить» | Идемпотентность | Одна заявка, одно вложение |
| Файл без отправки заявки | Очистка сирот | Вложение удаляется по таймауту |
| Доступ к чужому вложению | Проверка прав | Отказ, файл не отдаётся |
| Проверка не завершена | Статус в карточке | checking, менеджер видит состояние |
Прогонять это стоит не только на «идеальном» браузере, но и на медленном соединении, на мобильном, с файлами реальных размеров из вашей практики. Именно там вылезают обрывы, таймауты и гонки между загрузкой и отправкой.
Практикум: собрать цепочку на одном условном примере
Возьмём условную компанию, которая принимает заявки на расчёт через веб-форму. Сейчас файл отправляется вместе с формой одним запросом, лимит не задан, проверки типа нет, статус клиенту не показывается. Менеджеры регулярно получают заявки без вложений и не понимают почему.
Шаг первый — развести загрузку и отправку. Файл грузится отдельным запросом, получает attachment_id, форма отправляется с этим идентификатором. Клиент видит прогресс и статус.
Шаг второй — ввести статусы. uploading, received, checking, accepted, rejected. Кнопка отправки активна только при accepted или явном отказе от вложения.
Шаг третий — проверка на сервере. Белый список типов, проверка по сигнатуре, лимит размера, переименование, хранение вне публичного корня, отдача через проверку прав.
Шаг четвёртый — идемпотентность и очистка. Ключ заявки от двойных отправок, таймаут для вложений без заявки.
Шаг пятый — экран менеджера. Вложения в карточке со статусом, причиной отклонения и кнопкой скачивания через проверку прав.
Шаг шестой — чек-лист из таблицы выше, прогнанный на медленном соединении и мобильном.
Где заканчивается форма и начинается инфраструктура
Даже идеально спроектированная форма упирается в инфраструктуру: где хранятся файлы, как они резервируются, что происходит при переполнении диска, как быстро отдаётся файл на скачивание. Это уже не про HTML и не про валидацию, а про то, насколько надёжно ваше хранилище и как оно связано с заявками.
Полезно заранее ответить на несколько вопросов. Что происходит с вложением, если заявку удалили, — файл удаляется вместе с ней или остаётся? Кто имеет право удалять вложения и есть ли журнал таких действий? Как долго хранятся файлы и что говорит об этом политика компании и применимые документы? Ответы на эти вопросы лучше получить у уполномоченного сотрудника до запуска, а не после инцидента.
Ещё один слой — наблюдаемость. Если вы не логируете переходы статусов вложения, вы не сможете разобрать жалобу «файл не дошёл». Лог должен фиксировать: когда файл принят, когда проверен, когда привязан к заявке, когда отклонён и почему. Без этого разбор превращается в гадание.
Что стоит обсудить с разработчиком до старта
Если форму делает подрядчик или внутренняя команда, разговор полезно начать не с «сделайте загрузку файлов», а с конкретных вопросов. Какой лимит размера и откуда он взялся? Какие типы разрешены и почему именно они? Что происходит при обрыве — возобновление, повтор или отказ? Как проверяется содержимое? Где хранятся файлы и кто к ним имеет доступ? Как вложение связано с заявкой и что видит менеджер?
Ссылки, на которые опирается методика
Разбор выше — самостоятельная методика, но опирается на несколько первичных источников, которые полезно прочитать целиком, а не в пересказе. MDN описывает, как браузер даёт странице доступ к файлам через File API и что именно доступно на стороне клиента: Using files from web applications. OWASP перечисляет практики безопасной загрузки: белый список типов, проверку содержимого, хранение вне публичного корня, контролируемую отдачу — File Upload Cheat Sheet. Паттерн асинхронного запрос-ответа полезен как ориентир для длительных операций и разделения приёма и результата — Asynchronous request-reply pattern.
Эти источники описывают общие механизмы платформ и практики, а не функции конкретного продукта. Их наличие не означает, что в вашей системе уже есть возобновляемая загрузка, проверка по сигнатуре или журнал статусов — это то, что нужно реализовать или проверить отдельно.
Сводим вместе: что должно быть правдой к запуску
К моменту, когда форма уходит в бой, полезно, чтобы одновременно выполнялись несколько условий. Клиент видит разницу между «выбран», «загружается», «проверяется» и «принят». Кнопка отправки не активна, пока вложение не принято. Сервер проверяет тип по содержимому, а не по расширению, и хранит файлы вне публичного доступа. Повторная отправка не плодит заявки. Вложения без заявки не копятся вечно. Менеджер в карточке видит статус и причину отклонения. Доступ к файлу проверяется по правам. И есть чек-лист, который прогоняется на медленном соединении, а не только на быстром.
Это не про «эффективность» и не про рост продаж. Это про то, чтобы карточка заявки и реальность совпадали. Клиент, который приложил смету, должен быть уверен, что она дошла, а менеджер — видеть её там, где ожидает.
Начните с одного файла и карточки заявки
Не нужно переделывать всё сразу. Возьмите одну текущую заявку, к которой, по словам клиента, был приложен файл, и откройте её карточку. Если вложения там нет, а клиент уверен, что прикладывал, — вы нашли разрыв между «выбрал» и «принят». Дальше пройдите по цепочке: где файл теряется, на каком статусе, что видит клиент и что видит менеджер. Этого одного разбора обычно достаточно, чтобы понять, нужна ли раздельная загрузка, статусы и проверка содержимого — или проблема в одном конкретном месте. Начните с этой заявки, а не с переписывания формы целиком.
Смежный вопрос разобран отдельно: Аналитика форм на сайте: от начала ввода до принятой заявки.
Источники и границы разбора
Источники и статус проверки на 9 ноября 2026. Первичные документы поясняют указанные механизмы; доступность и границы каждого источника отмечены ниже. Методические рекомендации и условные примеры составляют самостоятельную часть разбора.
- Разработка сайтов
Опубликованное направление работы 404ai. Предлагаемая в статье методика не означает, что каждое описанное действие уже доступно без настройки; состав решения согласуется отдельно.
- MDN: Using files from web applications
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.
- OWASP: File Upload Cheat Sheet
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.
- Microsoft: Asynchronous request-reply pattern
Первичная документация указанного инструмента или механизма. Практические рекомендации и условные эпизоды статьи — самостоятельная методика, а не описание готовых функций 404ai.