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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: REST API multimedia file upload issue fixed #32921

Merged
merged 3 commits into from
Apr 29, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ describe(
it("1. Check whether auto generated header is set and overidden", () => {
_.apiPage.CreateApi("FirstAPI");
_.apiPage.SelectPaneTab("Body");

// Assert binary file type and its autogenerated header
_.apiPage.SelectSubTab("BINARY");
_.apiPage.ValidateImportedHeaderParams(true, {
key: "content-type",
value: "application/octet-stream",
});

// Assert form urlencoded data format and its autogenerated header
_.apiPage.SelectPaneTab("Body");
_.apiPage.SelectSubTab("FORM_URLENCODED");
_.apiPage.ValidateImportedHeaderParams(true, {
key: "content-type",
Expand Down
1 change: 1 addition & 0 deletions app/client/cypress/support/Pages/ApiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class ApiPage {
| "JSON"
| "FORM_URLENCODED"
| "MULTIPART_FORM_DATA"
| "BINARY"
| "RAW",
) {
this.agHelper.GetNClick(this._bodySubTab(subTabName));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export enum ApiContentType {
JSON = "json",
FORM_URLENCODED = "x-www-form-urlencoded",
MULTIPART_FORM_DATA = "multi-part/form-data",
BINARY = "application/octet-stream",
RAW = "text/plain",
}

Expand All @@ -72,6 +73,7 @@ export const POST_BODY_FORMAT_OPTIONS: Record<
JSON: "application/json",
FORM_URLENCODED: "application/x-www-form-urlencoded",
MULTIPART_FORM_DATA: "multipart/form-data",
BINARY: "application/octet-stream",
RAW: "text/plain",
};

Expand Down
18 changes: 18 additions & 0 deletions app/client/src/pages/Editor/APIEditor/PostBodyData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ function PostBodyData(props: Props) {
/>
</JSONEditorFieldWrapper>
);
// This format is particularly used for uploading files, in this case
// From filepicker we can take base64 string and pass it to server
// which then decodes it and uploads the file to given URL
case POST_BODY_FORMAT_OPTIONS.BINARY:
return (
<JSONEditorFieldWrapper key={key}>
<DynamicTextField
border={CodeEditorBorder.ALL_SIDE}
dataTreePath={`${dataTreePath}.body`}
mode={EditorModes.TEXT_WITH_BINDING}
name="actionConfiguration.body"
placeholder={`{{\n\t{name: inputName.property, preference: dropdownName.property}\n}}`}
size={EditorSize.EXTENDED}
tabBehaviour={TabBehaviour.INDENT}
theme={theme}
/>
</JSONEditorFieldWrapper>
);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
Expand All @@ -50,6 +51,8 @@ public class DataUtils {

public static String FIELD_API_CONTENT_TYPE = "apiContentType";

public static String BASE64_DELIMITER = ";base64,";

/**
* this Gson builder has three parameters for creating a gson instances which is required to maintain the JSON as received
* setLenient() : allows parsing of JSONs which don't strictly adhere to RFC4627 (our older implementation is also more permissive)
Expand Down Expand Up @@ -99,6 +102,8 @@ public DataUtils() {
return parseMultipartFileData((List<Property>) body);
case MediaType.TEXT_PLAIN_VALUE:
return BodyInserters.fromValue((String) body);
case MediaType.APPLICATION_OCTET_STREAM_VALUE:
return parseMultimediaData((String) body);
default:
return BodyInserters.fromValue(((String) body).getBytes(StandardCharsets.ISO_8859_1));
}
Expand Down Expand Up @@ -154,6 +159,23 @@ public String parseFormData(List<Property> bodyFormData, Boolean encodeParamsTog
.collect(Collectors.joining("&"));
}

public BodyInserter<?, ?> parseMultimediaData(String requestBodyObj) {
// This decoding for base64 is required because of
// issue https://github.com/appsmithorg/appsmith/issues/32378
// According to this if we tried to upload any multimedia files (img, audio, video)
// It was not getting decoded before uploading on required URL
if (requestBodyObj.contains(BASE64_DELIMITER)) {
List<String> bodyArrayList = Arrays.asList(requestBodyObj.split(BASE64_DELIMITER));
requestBodyObj = bodyArrayList.get(bodyArrayList.size() - 1);

// Using mimeDecoder here, since base64 conversion by file picker widget follows mime standard
byte[] payload = Base64.getMimeDecoder().decode(bodyArrayList.get(bodyArrayList.size() - 1));
return BodyInserters.fromValue(payload);
}

return BodyInserters.fromValue(requestBodyObj);
}

public BodyInserter<?, ?> parseMultipartFileData(List<Property> bodyFormData) {
if (bodyFormData == null || bodyFormData.isEmpty()) {
return BodyInserters.fromValue(new byte[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2595,4 +2595,61 @@ public void test_DifferentialParsingStrategy_fromGson() {
})
.verifyComplete();
}

@Test
public void testBinaryFileUploadAPIWithMustacheBinding() {
DatasourceConfiguration dsConfig = new DatasourceConfiguration();
String baseUrl = String.format("http://%s:%s", mockEndpoint.getHostName(), mockEndpoint.getPort());
dsConfig.setUrl(baseUrl);

mockEndpoint.enqueue(new MockResponse().setBody("{}").addHeader("Content-Type", "application/json"));

ActionConfiguration actionConfig = new ActionConfiguration();
actionConfig.setAutoGeneratedHeaders(List.of(new Property("content-type", "application/octet-stream")));
actionConfig.setHttpMethod(HttpMethod.POST);
actionConfig.setBody("{{jsonFilePicker.files[0].data}}");

ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
List<Param> params = new ArrayList<>();
Param param1 = new Param();
param1.setKey("jsonFilePicker.files[0].data");
param1.setValue(
"data:application/json;base64,ewogICJwcm9wZXJ0eTEiOiAidmFsdWUxIiwKICAicHJvcGVydHkyIjogInZhbHVlMiIKfQo=");
param1.setClientDataType(ClientDataType.STRING);
params.add(param1);
executeActionDTO.setParams(params);

Mono<ActionExecutionResult> resultMono =
pluginExecutor.executeParameterized(null, executeActionDTO, dsConfig, actionConfig);
StepVerifier.create(resultMono)
.assertNext(result -> {
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());

try {
final RecordedRequest recordedRequest = mockEndpoint.takeRequest(30, TimeUnit.SECONDS);
assert recordedRequest != null;
final Buffer recordedRequestBody = recordedRequest.getBody();
byte[] bodyBytes = new byte[(int) recordedRequestBody.size()];

recordedRequestBody.readFully(bodyBytes);
recordedRequestBody.close();

String bodyString = new String(bodyBytes);

// This asserts that original file content is being decoded back from base64 encoding
// before calling the API mentioned in REST API action config to upload this file
assertTrue(
bodyString.contains(
"""
{
"property1": "value1",
"property2": "value2"
}"""));
} catch (EOFException | InterruptedException e) {
assert false : e.getMessage();
}
})
.verifyComplete();
}
}