halo-dev/halo · error · ServerWebInputException

Invalid multipart type of file

Error message

Invalid multipart type of file

What it means

UpgradeRequest.getFile() reads the multipart part named 'file' from the upgrade request. If that part exists but is not a FilePart (e.g. it is a plain form FieldPart with no filename/binary content), a ServerWebInputException (HTTP 400) 'Invalid multipart type of file' is thrown. This guards the upgrade-by-upload endpoint before the .zip extension is even checked.

Source

Thrown at application/src/main/java/run/halo/app/theme/endpoint/ThemeEndpoint.java:450

     *
     * @param uri remote URI of the theme ZIP file
     */
    public record UpgradeFromUriRequest(
            @Schema(requiredMode = REQUIRED) URI uri) {}

    public static class UpgradeRequest implements IUpgradeRequest {

        private final MultiValueMap<String, Part> multipartData;

        public UpgradeRequest(MultiValueMap<String, Part> multipartData) {
            this.multipartData = multipartData;
        }

        @Override
        public FilePart getFile() {
            var part = multipartData.getFirst("file");
            if (!(part instanceof FilePart filePart)) {
                throw new ServerWebInputException("Invalid multipart type of file");
            }
            if (!filePart.filename().endsWith(".zip")) {
                throw new ServerWebInputException("Only zip extension supported");
            }
            return filePart;
        }
    }

    private Mono<ServerResponse> upgrade(ServerRequest request) {
        // validate the theme first
        var name = request.pathVariable("name");
        return request.multipartData()
                .map(UpgradeRequest::new)
                .map(UpgradeRequest::getFile)
                .flatMap(filePart -> themeService.upgrade(name, filePart.content()))
                .flatMap((updatedTheme) -> templateEngineManager
                        .clearCache(updatedTheme.getMetadata().getName())
                        .thenReturn(updatedTheme))

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Send the file as a binary upload: curl -F 'file=@/path/theme.zip'.
  2. On the frontend, append a real File/Blob to FormData under the 'file' key.
  3. Verify the Content-Disposition of the 'file' part includes a filename parameter.
  4. Ensure the request Content-Type is multipart/form-data (set automatically by FormData; do not set it manually).

Example fix

// before (wrong): text field, not a file
//   curl -F 'file=something' /api/.../themes/name/upgrade
// after: binary file part
//   curl -F 'file=@theme.zip' /api/.../themes/name/upgrade
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: ensure the file part is a real File before sending
const f = fileInput.files && fileInput.files[0];
if (!(f instanceof File)) {
    showError("Please choose a file to upload.");
    return;
}
const fd = new FormData();
fd.append("file", f, f.name);
// curl: curl -F 'file=@theme.zip' ...

Type guard

// JS: narrow to a real File (binary part)
function isBinaryFile(v) {
  return v instanceof File;
}

Try / catch

// Server-side, the endpoint already returns 400; map it for the client:
try { await upgradeTheme(fd); }
catch (e) { if (/Invalid multipart type/.test(e.message)) alert("Upload a file, not text."); throw e; }

Prevention

When it happens

Trigger: POST to the theme upgrade endpoint with a multipart body where the 'file' part was sent as a text form field (Content-Disposition: form-data; name="file" without a filename), or the part is otherwise not a FilePart. Common with hand-crafted curl/Postman requests using -F 'file=value' instead of -F 'file=@path'.

Common situations: Wrong curl flag (-F file=value vs -F file=@file.zip); a frontend sending a string instead of a File object in FormData; a proxy normalizing the multipart boundary/disposition.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/db98051460284612. Report an issue: GitHub.