Retrofit 2
Multipart Form Data
Android Development
Image Upload
HTTP Client

POST Multipart Form Data using Retrofit 2.0 including image

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Uploading an image with form fields is a standard Android workflow for profiles, tickets, and content publishing. Retrofit 2 handles this cleanly with multipart requests, but small mistakes in request body creation often cause server errors. A robust setup includes correct MIME types, stable URI handling, and explicit endpoint contracts.

Defining the Retrofit API Contract

Start with a clear service interface. Use @Multipart and split binary and text parameters into separate parts.

kotlin
1import okhttp3.MultipartBody
2import okhttp3.RequestBody
3import retrofit2.Response
4import retrofit2.http.Multipart
5import retrofit2.http.POST
6import retrofit2.http.Part
7
8interface UploadApi {
9    @Multipart
10    @POST("v1/profile/photo")
11    suspend fun uploadProfile(
12        @Part image: MultipartBody.Part,
13        @Part("displayName") displayName: RequestBody,
14        @Part("bio") bio: RequestBody
15    ): Response<UploadResponse>
16}
17
18data class UploadResponse(
19    val id: String,
20    val imageUrl: String
21)

This contract maps directly to a typical multipart form where one part is file data and other parts are text fields.

Creating Request Parts Correctly

On Android, you often start from a content Uri chosen in a picker. Convert it to a temporary file or stream safely, then build parts with accurate media types.

kotlin
1import android.content.ContentResolver
2import android.content.Context
3import android.net.Uri
4import okhttp3.MediaType.Companion.toMediaType
5import okhttp3.MultipartBody
6import okhttp3.RequestBody
7import okhttp3.RequestBody.Companion.asRequestBody
8import okhttp3.RequestBody.Companion.toRequestBody
9import java.io.File
10
11fun createImagePart(context: Context, uri: Uri): MultipartBody.Part {
12    val input = context.contentResolver.openInputStream(uri)
13        ?: error("Cannot open input stream")
14
15    val tempFile = File.createTempFile("upload_", ".jpg", context.cacheDir)
16    tempFile.outputStream().use output ->
17        input.use it.copyTo(output)
18    }
19
20    val mediaType = "image/jpeg".toMediaType()
21    val body = tempFile.asRequestBody(mediaType)
22
23    return MultipartBody.Part.createFormData(
24        name = "image",
25        filename = tempFile.name,
26        body = body
27    )
28}
29
30fun String.toPart(): RequestBody =
31    this.toRequestBody("text/plain".toMediaType())

Hardcoding a wrong media type is a common cause of backend rejection. If file type can vary, derive MIME via ContentResolver and fallback conservatively.

Executing the Upload with Coroutines

Run network calls off the main thread using coroutines and expose structured success or failure to the UI layer.

kotlin
1import kotlinx.coroutines.Dispatchers
2import kotlinx.coroutines.withContext
3
4class UploadRepository(private val api: UploadApi, private val context: Context) {
5
6    suspend fun upload(uri: Uri, displayName: String, bio: String): Result<String> {
7        return withContext(Dispatchers.IO) {
8            runCatching {
9                val imagePart = createImagePart(context, uri)
10                val namePart = displayName.toPart()
11                val bioPart = bio.toPart()
12
13                val response = api.uploadProfile(imagePart, namePart, bioPart)
14
15                if (!response.isSuccessful) {
16                    error("Upload failed with code ${response.code()}")
17                }
18
19                val body = response.body() ?: error("Empty response body")
20                body.imageUrl
21            }
22        }
23    }
24}

This pattern keeps concerns separated: API contract, part creation, and use case flow.

Debugging Multipart Requests

If server parsing fails, inspect outgoing requests with an OkHttp logging interceptor or a proxy tool. Validate part names, not only values. Backend code usually expects exact field names such as image or avatar, and mismatch causes silent null values server side.

Also verify server limits. Large files can trigger 413 responses or framework-level exceptions before controller code runs.

Hardening the Upload Flow

Production apps need guardrails around the upload call. Validate local file size before network work starts, and fail fast with a clear message when the file is above server policy. Early validation saves battery and avoids long transfers that are guaranteed to fail.

Treat duplicate submission as a first-class case. Users often tap the action button multiple times during slow uploads. Disable the button while upload is in progress, or send an idempotency key so backend logic can collapse duplicate requests safely.

For reliability on unstable networks, separate media upload from profile metadata update. First upload the file and obtain a media id or URL. Then send metadata in a second call. That split gives cleaner retry boundaries and makes partial failure recovery much easier.

kotlin
1sealed interface UploadState {
2    data object Idle : UploadState
3    data object Uploading : UploadState
4    data class Success(val url: String) : UploadState
5    data class Error(val message: String) : UploadState
6}

A small state model like this keeps the UI deterministic across retries, rotation, and process recreation.

Common Pitfalls

  • Using wrong part names. Fix by matching backend field names exactly in createFormData and @Part keys.
  • Sending file URI string instead of binary content. Fix by opening stream and creating MultipartBody.Part from bytes.
  • Forgetting MIME type correctness. Fix by using detected media type and validating backend expectations.
  • Running upload on main thread. Fix by executing in Dispatchers.IO.
  • Ignoring non success responses. Fix by checking isSuccessful and parsing error payload for diagnostics.

Summary

  • Use @Multipart with one MultipartBody.Part for image and RequestBody for text fields.
  • Convert content Uri to request body safely through streams.
  • Keep API contract and upload orchestration cleanly separated.
  • Validate field names and MIME types first when debugging.
  • Treat response codes and empty payloads as explicit failure paths.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.