Skip to content

Commit

Permalink
feat(http): support for FormData requests (#6708)
Browse files Browse the repository at this point in the history
Co-authored-by: Bruno Sales Cardoso <[email protected]>
  • Loading branch information
kjr-lh and bsalesc authored Jul 11, 2023
1 parent 06fc5f1 commit 849c564
Show file tree
Hide file tree
Showing 10 changed files with 460 additions and 136 deletions.
142 changes: 101 additions & 41 deletions android/capacitor/src/main/assets/native-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,57 @@ var nativeBridge = (function (exports) {
// For removing exports for iOS/Android, keep let for reassignment
// eslint-disable-next-line
let dummy = {};
const readFileAsBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const data = reader.result;
resolve(btoa(data));
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
const convertFormData = async (formData) => {
const newFormData = [];
for (const pair of formData.entries()) {
const [key, value] = pair;
if (value instanceof File) {
const base64File = await readFileAsBase64(value);
newFormData.push({
key,
value: base64File,
type: 'base64File',
contentType: value.type,
fileName: value.name,
});
}
else {
newFormData.push({ key, value, type: 'string' });
}
}
return newFormData;
};
const convertBody = async (body) => {
if (body instanceof FormData) {
const formData = await convertFormData(body);
const boundary = `${Date.now()}`;
return {
data: formData,
type: 'formData',
headers: {
'Content-Type': `multipart/form-data; boundary=--${boundary}`,
},
};
}
else if (body instanceof File) {
const fileData = await readFileAsBase64(body);
return {
data: fileData,
type: 'file',
headers: { 'Content-Type': body.type },
};
}
return { data: body, type: 'json' };
};
const initBridge = (w) => {
const getPlatformId = (win) => {
var _a, _b;
Expand Down Expand Up @@ -368,7 +419,6 @@ var nativeBridge = (function (exports) {
if (doPatchHttp) {
// fetch patch
window.fetch = async (resource, options) => {
var _a;
if (!(resource.toString().startsWith('http:') ||
resource.toString().startsWith('https:'))) {
return win.CapacitorWebFetch(resource, options);
Expand All @@ -377,18 +427,23 @@ var nativeBridge = (function (exports) {
console.time(tag);
try {
// intercept request & pass to the bridge
let headers = options === null || options === void 0 ? void 0 : options.headers;
const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || undefined);
let optionHeaders = options === null || options === void 0 ? void 0 : options.headers;
if ((options === null || options === void 0 ? void 0 : options.headers) instanceof Headers) {
headers = Object.fromEntries(options.headers.entries());
optionHeaders = Object.fromEntries(options.headers.entries());
}
const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', {
url: resource,
method: (options === null || options === void 0 ? void 0 : options.method) ? options.method : undefined,
data: (options === null || options === void 0 ? void 0 : options.body) ? options.body : undefined,
headers: headers,
data: requestData,
dataType: type,
headers: Object.assign(Object.assign({}, headers), optionHeaders),
});
let data = ((_a = nativeResponse.headers['Content-Type']) === null || _a === void 0 ? void 0 : _a.startsWith('application/json'))
? JSON.stringify(nativeResponse.data) : nativeResponse.data;
const contentType = nativeResponse.headers['Content-Type'] ||
nativeResponse.headers['content-type'];
let data = (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('application/json'))
? JSON.stringify(nativeResponse.data)
: nativeResponse.data;
// use null data for 204 No Content HTTP response
if (nativeResponse.status === 204) {
data = null;
Expand Down Expand Up @@ -518,44 +573,49 @@ var nativeBridge = (function (exports) {
console.time(tag);
try {
this.readyState = 2;
// intercept request & pass to the bridge
cap
.nativePromise('CapacitorHttp', 'request', {
url: this._url,
method: this._method,
data: body !== null ? body : undefined,
headers: this._headers != null && Object.keys(this._headers).length > 0
convertBody(body).then(({ data, type, headers }) => {
const otherHeaders = this._headers != null && Object.keys(this._headers).length > 0
? this._headers
: undefined,
})
.then((nativeResponse) => {
var _a;
// intercept & parse response before returning
if (this.readyState == 2) {
: undefined;
// intercept request & pass to the bridge
cap
.nativePromise('CapacitorHttp', 'request', {
url: this._url,
method: this._method,
data: data !== null ? data : undefined,
headers: Object.assign(Object.assign({}, headers), otherHeaders),
dataType: type,
})
.then((nativeResponse) => {
var _a;
// intercept & parse response before returning
if (this.readyState == 2) {
this.dispatchEvent(new Event('loadstart'));
this._headers = nativeResponse.headers;
this.status = nativeResponse.status;
this.response = nativeResponse.data;
this.responseText = ((_a = nativeResponse.headers['Content-Type']) === null || _a === void 0 ? void 0 : _a.startsWith('application/json'))
? JSON.stringify(nativeResponse.data)
: nativeResponse.data;
this.responseURL = nativeResponse.url;
this.readyState = 4;
this.dispatchEvent(new Event('load'));
this.dispatchEvent(new Event('loadend'));
}
console.timeEnd(tag);
})
.catch((error) => {
this.dispatchEvent(new Event('loadstart'));
this._headers = nativeResponse.headers;
this.status = nativeResponse.status;
this.response = nativeResponse.data;
this.responseText = ((_a = nativeResponse.headers['Content-Type']) === null || _a === void 0 ? void 0 : _a.startsWith('application/json'))
? JSON.stringify(nativeResponse.data) : nativeResponse.data;
this.responseURL = nativeResponse.url;
this.status = error.status;
this._headers = error.headers;
this.response = error.data;
this.responseText = JSON.stringify(error.data);
this.responseURL = error.url;
this.readyState = 4;
this.dispatchEvent(new Event('load'));
this.dispatchEvent(new Event('error'));
this.dispatchEvent(new Event('loadend'));
}
console.timeEnd(tag);
})
.catch((error) => {
this.dispatchEvent(new Event('loadstart'));
this.status = error.status;
this._headers = error.headers;
this.response = error.data;
this.responseText = JSON.stringify(error.data);
this.responseURL = error.url;
this.readyState = 4;
this.dispatchEvent(new Event('error'));
this.dispatchEvent(new Event('loadend'));
console.timeEnd(tag);
console.timeEnd(tag);
});
});
}
catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
import java.net.URLEncoder;
import java.net.UnknownServiceException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.json.JSONException;
import org.json.JSONObject;

public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection {

Expand Down Expand Up @@ -173,7 +175,7 @@ public void setDoOutput(boolean shouldDoOutput) {
* @throws JSONException
* @throws IOException
*/
public void setRequestBody(PluginCall call, JSValue body) throws JSONException, IOException {
public void setRequestBody(PluginCall call, JSValue body, String bodyType) throws JSONException, IOException {
String contentType = connection.getRequestProperty("Content-Type");
String dataString = "";

Expand All @@ -192,6 +194,15 @@ public void setRequestBody(PluginCall call, JSValue body) throws JSONException,
dataString = call.getString("data");
}
this.writeRequestBody(dataString != null ? dataString : "");
} else if (bodyType.equals("file")) {
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
os.write(Base64.getDecoder().decode(body.toString()));
}
os.flush();
}
} else if (bodyType.equals("formData")) {
this.writeFormDataRequestBody(contentType, body.toJSArray());
} else {
this.writeRequestBody(body.toString());
}
Expand All @@ -209,6 +220,47 @@ private void writeRequestBody(String body) throws IOException {
}
}

private void writeFormDataRequestBody(String contentType, 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 = "--";

for (Object e : entries.toList()) {
if (e instanceof JSONObject) {
JSONObject entry = (JSONObject) e;
String type = entry.getString("type");
String key = entry.getString("key");
String value = entry.getString("value");
if (type.equals("string")) {
os.writeBytes(twoHyphens + boundary + lineEnd);
os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"" + lineEnd + lineEnd);
os.writeBytes(value);
os.writeBytes(lineEnd);
} else if (type.equals("base64File")) {
String fileName = entry.getString("fileName");
String fileContentType = entry.getString("contentType");

os.writeBytes(twoHyphens + boundary + lineEnd);
os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + fileName + "\"" + lineEnd);
os.writeBytes("Content-Type: " + fileContentType + lineEnd);
os.writeBytes("Content-Transfer-Encoding: binary" + lineEnd);
os.writeBytes(lineEnd);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
os.write(Base64.getDecoder().decode(value));
}

os.writeBytes(lineEnd);
}
}
}

os.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
os.flush();
}
}

/**
* Opens a communications link to the resource referenced by this
* URL, if such a connection has not already been established.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ public static JSObject request(PluginCall call, String httpMethod, Bridge bridge
Boolean disableRedirects = call.getBoolean("disableRedirects");
Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
ResponseType responseType = ResponseType.parse(call.getString("responseType"));
String dataType = call.getString("dataType");

String method = httpMethod != null ? httpMethod.toUpperCase(Locale.ROOT) : call.getString("method", "GET").toUpperCase(Locale.ROOT);

Expand Down Expand Up @@ -406,7 +407,7 @@ public static JSObject request(PluginCall call, String httpMethod, Bridge bridge
JSValue data = new JSValue(call, "data");
if (data.getValue() != null) {
connection.setDoOutput(true);
connection.setRequestBody(call, data);
connection.setRequestBody(call, data, dataType);
}
}

Expand Down
Loading

0 comments on commit 849c564

Please sign in to comment.