Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.json.JSONException;
Expand Down Expand Up @@ -224,7 +225,16 @@ public void setRequestBody(PluginCall call, JSValue body, String bodyType) throw
this.writeRequestBody(body.toString());
}
} else if (bodyType != null && bodyType.equals("formData")) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if condition on line 197 is never reached maybe only on xhr requests or on both.
In native-bridge.js, it seems the Content-Type header is not being set. Could this be related to the issue?

Additionally, I am encountering a problem where my POST requests with (angular httpClient) multipart/form-data result in an empty body. This might be relevant to the bug described here: #7579.

android
https://github.com/ionic-team/capacitor/blob/main/android/capacitor/src/main/assets/native-bridge.js#L129
Called here (fetch proxy possible working) -> https://github.com/ionic-team/capacitor/blob/main/android/capacitor/src/main/assets/native-bridge.js#L493
Called here (xhr proxy bugged) -> https://github.com/ionic-team/capacitor/blob/main/android/capacitor/src/main/assets/native-bridge.js#L623

ios
https://github.com/ionic-team/capacitor/blob/main/ios/Capacitor/Capacitor/assets/native-bridge.js#L129
called here (fetch proxy maybe bugged) -> https://github.com/ionic-team/capacitor/blob/main/ios/Capacitor/Capacitor/assets/native-bridge.js#L493
called here (xhr proxy bugged) -> https://github.com/ionic-team/capacitor/blob/main/ios/Capacitor/Capacitor/assets/native-bridge.js#L623

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is definitely reached for regular fetch requests. Please feel free to checkout the provided example in the PR description which shows a minimal example using a fetch request. I also just updated the example to the latest capacitor version.

image

I am not sure that your issue is really related to the bug I tried to address here. This PR is only about wrong handling of form data boundaries.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, never mind—I found the issue. Sorry for the interruption.

If you don’t set the Content-Type header explicitly, the CapacitorHttp proxy will not automatically add a default Content-Type header and ignore the body.
In web the browser will handle this and add a default Content-Type...

I tested this behavior using your example app.

this.writeFormDataRequestBody(contentType, body.toJSArray());
String boundary = extractBoundaryFromContentType(contentType);
if (boundary == null) {
// If no boundary is provided, generate a random one and set the Content-Type header accordingly
// or otherwise servers will not be able to parse the request body. Browsers do this automatically
// but here we need to do this manually in order to comply with browser api behavior.
boundary = UUID.randomUUID().toString();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this part was missing in the iOS implementation but present for Android already.

connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
}

this.writeFormDataRequestBody(boundary, body.toJSArray());
} else {
this.writeRequestBody(body.toString());
}
Expand Down Expand Up @@ -260,9 +270,8 @@ private void writeObjectRequestBody(JSObject object) throws IOException, JSONExc
}
}

private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException {
private void writeFormDataRequestBody(String boundary, JSArray entries) throws IOException, JSONException {
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
String boundary = contentType.split(";")[1].split("=")[1];
String lineEnd = "\r\n";
String twoHyphens = "--";

Expand Down Expand Up @@ -303,6 +312,39 @@ private void writeFormDataRequestBody(String contentType, JSArray entries) throw
}
}

/**
* Extracts the boundary value from the `Content-Type` header for multipart/form-data requests, if provided.
*
* The boundary value might be surrounded by double quotes (") which will be stripped away.
*
* @param contentType The `Content-Type` header string.
* @return The boundary value if found, otherwise `null`.
*/
public static String extractBoundaryFromContentType(String contentType) {
String boundaryPrefix = "boundary=";
int boundaryIndex = contentType.indexOf(boundaryPrefix);
if (boundaryIndex == -1) {
return null;
}

// Extract the substring starting right after "boundary="
String boundary = contentType.substring(boundaryIndex + boundaryPrefix.length());

// Find the end of the boundary value by looking for the next ";"
int endIndex = boundary.indexOf(";");
if (endIndex != -1) {
boundary = boundary.substring(0, endIndex);
}

// Remove surrounding double quotes if present
boundary = boundary.trim();
if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
boundary = boundary.substring(1, boundary.length() - 1);
}

return boundary;
}

/**
* Opens a communications link to the resource referenced by this
* URL, if such a connection has not already been established.
Expand Down
25 changes: 23 additions & 2 deletions ios/Capacitor/Capacitor/Plugins/CapacitorUrlRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {

var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
if contentType.contains("boundary="), let contentBoundary = extractBoundary(from: contentType) {
boundary = contentBoundary
} else {
overrideContentType(boundary)
Expand All @@ -86,6 +86,27 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
headers["Content-Type"] = contentType
}

/**
Extracts the boundary value of the `content-type` header for multiplart/form-data requests, if provided
The boundary value might be surrounded by double quotes (") which will be stripped away.
*/
private func extractBoundary(from contentType: String) -> String? {
if let boundaryRange = contentType.range(of: "boundary=") {
var boundary = contentType[boundaryRange.upperBound...]
if let endRange = boundary.range(of: ";") {
boundary = boundary[..<endRange.lowerBound]
}

if boundary.hasPrefix("\"") && boundary.hasSuffix("\"") {
return String(boundary.dropFirst().dropLast())
} else {
return String(boundary)
}
}

return nil
}

public func getRequestDataAsString(_ data: JSValue) throws -> Data {
guard let stringData = data as? String else {
Expand All @@ -110,7 +131,7 @@ open class CapacitorUrlRequest: NSObject, URLSessionTaskDelegate {
}
var data = Data()
var boundary = UUID().uuidString
if contentType.contains("="), let contentBoundary = contentType.components(separatedBy: "=").last {
if contentType.contains("boundary="), let contentBoundary = extractBoundary(from: contentType) {
boundary = contentBoundary
} else {
overrideContentType(boundary)
Expand Down