VolleyMultipartRequest For Image Upload From Android App To PHP API
The 400 Bad Request HTML error is generated directly by the web server (Apache/Nginx) before your PHP script ever runs.
This occurs because Volley's StringRequest sends parameters using application/x-www-form-urlencoded. When you encode an image to a large Base64 string, Volley URL-encodes it, creating a massive continuous form string. Web servers reject oversized URL-encoded POST field payloads with an HTTP 400 error.
Root Causes
Excessive Form Body Size: Apache has strict default limits for single POST form parameters (
LimitRequestBodyand field size limits).Base64 URL-Encoding Bloat: Converting binary data to Base64 increases its size by ~33%. URL-encoding that Base64 string expands it even further.
Use Multipart Volley Request (Best Practice)
For handling binary files without Base64 overhead, implement a custom Volley MultipartRequest class to send image files directly via multipart/form-data.Since a 211 KB file payload (~280 KB Base64) is well within server POST size limits, the HTTP 400 Bad Request is no longer caused by payload size. Instead, it is caused by ModSecurity/WAF blocking Base64 inside form data, or hidden non-ASCII characters in the API URL.
Step 1: Create VolleyMultipartRequest.java
package nanoakhi.rcfinternal.urgency;
import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.toolbox.HttpHeaderParser;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
public abstract class VolleyMultipartRequest extends Request<String> {
private final Response.Listener<String> mListener;
private final String boundary = "apiclient-" + System.currentTimeMillis();
public VolleyMultipartRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
super(method, url, errorListener);
this.mListener = listener;
}
@Override.
public String getBodyContentType() {
return "multipart/form-data; boundary=" + boundary;
}
@Override
public byte[] getBody() throws AuthFailureError {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
Map<String, String> params = getParams();
if (params != null && params.size() > 0) {
for (Map.Entry<String, String> entry : params.entrySet()) {
dos.writeBytes("--" + boundary + "\r\n");
dos.writeBytes("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n");
dos.writeBytes(entry.getValue() + "\r\n");
}
}
Map<String, DataPart> data = getByteData();
if (data != null && data.size() > 0) {
for (Map.Entry<String, DataPart> entry : data.entrySet()) {
DataPart dp = entry.getValue();
dos.writeBytes("--" + boundary + "\r\n");
dos.writeBytes("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"; filename=\"" + dp.getFileName() + "\"\r\n");
dos.writeBytes("Content-Type: " + dp.getType() + "\r\n\r\n");
dos.write(dp.getContent());
dos.writeBytes("\r\n");
}
}
dos.writeBytes("--" + boundary + "--\r\n");
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
protected abstract Map<String, DataPart> getByteData() throws AuthFailureError;
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
try {
String utf8String = new String(response.data, HttpHeaderParser.parseCharset(response.headers, "utf-8"));
return Response.success(utf8String, HttpHeaderParser.parseCacheHeaders(response));
} catch (Exception e) {
return Response.error(new com.android.volley.VolleyError(response));
}
}
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
public static class DataPart {
private String fileName;
private byte[] content;
private String type;
public DataPart(String fileName, byte[] content, String type) {
this.fileName = fileName;
this.content = content;
this.type = type;
}
public String getFileName() { return fileName; }
public byte[] getContent() { return content; }
public String getType() { return type; }
}
}
Step 2: Read File via $_FILES in PHP Add this part in PHP API
if (isset($_FILES['URG_IMG']) && $_FILES['URG_IMG']['error'] == UPLOAD_ERR_OK) {
$image_data = file_get_contents($_FILES['URG_IMG']['tmp_name']);
}
Updated Android Front-End (submitCertificateWithImage) Let suppose you made a method
private void submitCertificateWithImage() {
if (!validateFields()) {
return;
}
progressDialog.show();
VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(
Request.Method.POST,
response -> {
progressDialog.dismiss();
try {
Log.d("API_RESPONSE", response);
JSONObject obj = new JSONObject(response);
boolean success = obj.getBoolean("success");
String msg = obj.getString("message");
Toast.makeText(new_certificate_request.this, msg, Toast.LENGTH_LONG).show();
if (success) {
finish();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(new_certificate_request.this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
},
error -> {
progressDialog.dismiss();
String errorMsg = "Network Error";
if (error.networkResponse != null && error.networkResponse.data != null) {
try {
errorMsg = new String(error.networkResponse.data, "UTF-8");
} catch (Exception e) {
errorMsg = error.getMessage();
}
}
Log.e("API_ERROR", "Error: " + errorMsg);
Toast.makeText(new_certificate_request.this, "Error: " + errorMsg, Toast.LENGTH_LONG).show();
}
) {
@Override
protected Map<String, String> getParams() {
Map<String, String> params = new HashMap<>();
// Standard text fields
params.put("PL_NUMBER", etPLNumber.getText().toString().trim());
// Priority
int selectedPriorityId = rgPriority.getCheckedRadioButtonId();
String priority = "NORMAL";
if (selectedPriorityId == R.id.rbUrgent) {
priority = "URGENT";
} else if (selectedPriorityId == R.id.rbCritical) {
priority = "CRITICAL";
}
params.put("PRIORITY", priority);
params.put("FORWARD_TO", selectedEmpNo);
params.put("WHATSAPP", selectedWhatsapp);
return params;
}
@Override
protected Map<String, DataPart> getByteData() {
Map<String, DataPart> params = new HashMap<>();
if (imagePath != null) {
try {
File imageFile = new File(imagePath);
if (imageFile.exists()) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options);
if (bitmap != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, baos);
byte[] imageBytes = baos.toByteArray();
// Attach binary data directly without Base64 encoding
params.put("URG_IMG", new DataPart("urgency_image.jpg", imageBytes, "image/jpeg"));
Log.d("MULTIPART", "Binary image size: " + imageBytes.length + " bytes");
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e("MULTIPART_ERROR", "Error preparing image binary: " + e.getMessage());
}
}
return params;
}
};
multipartRequest.setRetryPolicy(new DefaultRetryPolicy(
30000,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
));
Volley.newRequestQueue(this).add(multipartRequest);
}
No Base64 Overhead: Converts raw JPEG bytes into standard stream boundaries (multipart/form-data), which avoids WAF / Apache 400 rejection limits
PHP Temporary Storage: PHP populates the uploaded file into $_FILES['URG_IMG']['tmp_name']
Oracle Compatibility: file_get_contents($_FILES['URG_IMG']['tmp_name']) returns the exact byte stream expected by Oracle's $blob->save($image_data) method.


Post a Comment
0Comments