Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(http): support for FormData requests #6708

Merged
merged 5 commits into from Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
142 changes: 101 additions & 41 deletions android/capacitor/src/main/assets/native-bridge.js
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
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
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