Skip to content

Commit b886b69

Browse files
authored
Refactor error handling for empty JSON and XML payloads (#567)
* [Automated] Update the native jar versions * Refactor error handling for empty JSON and XML payloads * Update changelog * Add the error cause to the no content errors * Refactor the implementation * Add test cases
1 parent 9c14c6f commit b886b69

File tree

7 files changed

+165
-20
lines changed

7 files changed

+165
-20
lines changed

ballerina/Ballerina.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
org = "ballerina"
33
name = "mime"
4-
version = "2.12.0"
4+
version = "2.12.1"
55
authors = ["Ballerina"]
66
keywords = ["mime", "multipart", "entity"]
77
repository = "https://github.com/ballerina-platform/module-ballerina-mime"
@@ -15,11 +15,11 @@ graalvmCompatible = true
1515
[[platform.java21.dependency]]
1616
groupId = "io.ballerina.stdlib"
1717
artifactId = "mime-native"
18-
version = "2.12.0"
19-
path = "../native/build/libs/mime-native-2.12.0.jar"
18+
version = "2.12.1"
19+
path = "../native/build/libs/mime-native-2.12.1-SNAPSHOT.jar"
2020

2121
[[platform.java21.dependency]]
22-
path = "../test-utils/build/libs/mime-test-utils-2.12.0.jar"
22+
path = "../test-utils/build/libs/mime-test-utils-2.12.1-SNAPSHOT.jar"
2323
scope = "testOnly"
2424

2525
[[platform.java21.dependency]]

ballerina/Dependencies.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ modules = [
120120
[[package]]
121121
org = "ballerina"
122122
name = "mime"
123-
version = "2.12.0"
123+
version = "2.12.1"
124124
dependencies = [
125125
{org = "ballerina", name = "io"},
126126
{org = "ballerina", name = "jballerina.java"},
@@ -136,7 +136,7 @@ modules = [
136136
[[package]]
137137
org = "ballerina"
138138
name = "observe"
139-
version = "1.5.0"
139+
version = "1.5.1"
140140
dependencies = [
141141
{org = "ballerina", name = "jballerina.java"}
142142
]

ballerina/tests/mime_test.bal

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,71 @@ isolated function getPartsFromInvalidChannel() {
15861586
}
15871587
}
15881588

1589+
@test:Config {}
1590+
function testEmptyJson1() {
1591+
Entity entity = new;
1592+
byte[][] emptyPayload = [];
1593+
entity.setByteStream(emptyPayload.toStream());
1594+
json|error content = entity.getJson();
1595+
if content is json {
1596+
test:assertFail("Expected an error but got JSON content: " + content.toString());
1597+
} else {
1598+
test:assertTrue(content is ParserError);
1599+
error? cause = content.cause();
1600+
if cause is () {
1601+
test:assertFail("Expected a cause for the ParserError but got none.");
1602+
}
1603+
test:assertTrue(cause is NoContentError);
1604+
test:assertEquals(cause.message(), "empty JSON document");
1605+
}
1606+
}
1607+
1608+
@test:Config {}
1609+
function testEmptyJson2() {
1610+
Entity entity = new;
1611+
entity.setText("");
1612+
json|error content = entity.getJson();
1613+
if content is json {
1614+
test:assertFail("Expected an error but got JSON content: " + content.toString());
1615+
} else {
1616+
test:assertTrue(content is ParserError);
1617+
error? cause = content.cause();
1618+
if cause is () {
1619+
test:assertFail("Expected a cause for the ParserError but got none.");
1620+
}
1621+
test:assertTrue(cause is NoContentError);
1622+
test:assertEquals(cause.message(), "empty JSON document");
1623+
}
1624+
}
1625+
1626+
@test:Config {}
1627+
function testEmptyXml1() {
1628+
Entity entity = new;
1629+
byte[][] emptyPayload = [];
1630+
entity.setByteStream(emptyPayload.toStream());
1631+
xml|error content = entity.getXml();
1632+
if content is xml {
1633+
test:assertFail("Expected an error but got XML content: " + content.toString());
1634+
} else {
1635+
test:assertTrue(content is ParserError);
1636+
error? cause = content.cause();
1637+
if cause is () {
1638+
test:assertFail("Expected a cause for the ParserError but got none.");
1639+
}
1640+
test:assertTrue(cause is NoContentError);
1641+
test:assertTrue(cause.message().includes("Empty xml payload"));
1642+
}
1643+
}
1644+
1645+
@test:Config {}
1646+
function testEmptyXml2() {
1647+
Entity entity = new;
1648+
entity.setText("");
1649+
xml|error content = entity.getXml();
1650+
// XML parser in Ballerina returns empty XML for empty string input
1651+
test:assertEquals(content, xml ``);
1652+
}
1653+
15891654
isolated function assertByteArray(byte[]|error returnResult, string expectValue) {
15901655
if returnResult is byte[] {
15911656
var value = strings:fromBytes(returnResult);

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ This file contains all the notable changes done to the Ballerina MIME package th
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
55
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
### Fixed
10+
11+
- [Add no content error for runtime parser no content exception](https://github.com/ballerina-platform/ballerina-library/issues/8475)
12+
713
## [2.11.0] - 2025-02-11
814
- This version maintains the latest dependency versions.
915

native/src/main/java/io/ballerina/stdlib/mime/nativeimpl/MimeDataSourceBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
2020

2121
import io.ballerina.runtime.api.creators.ValueCreator;
2222
import io.ballerina.runtime.api.types.Type;
23-
import io.ballerina.runtime.api.utils.JsonUtils;
2423
import io.ballerina.runtime.api.utils.StringUtils;
2524
import io.ballerina.runtime.api.utils.TypeUtils;
26-
import io.ballerina.runtime.api.utils.XmlUtils;
2725
import io.ballerina.runtime.api.values.BArray;
2826
import io.ballerina.runtime.api.values.BError;
2927
import io.ballerina.runtime.api.values.BObject;
@@ -43,6 +41,8 @@
4341
import static io.ballerina.stdlib.mime.util.MimeConstants.ENTITY_BYTE_CHANNEL;
4442
import static io.ballerina.stdlib.mime.util.MimeConstants.PARSER_ERROR;
4543
import static io.ballerina.stdlib.mime.util.MimeUtil.isNotNullAndEmpty;
44+
import static io.ballerina.stdlib.mime.util.MimeUtil.parseAsJson;
45+
import static io.ballerina.stdlib.mime.util.MimeUtil.parseAsXml;
4646

4747
/**
4848
* Utilities related to MIME entity body that can be built as a data source.
@@ -106,7 +106,7 @@ protected static Object getAlreadyBuiltJson(Object dataSource) {
106106
} else {
107107
// Else, build the JSON from the string representation of the payload.
108108
String payload = MimeUtil.getMessageAsString(dataSource);
109-
result = (BRefValue) JsonUtils.parse(payload);
109+
result = (BRefValue) parseAsJson(payload);
110110
}
111111
return result;
112112
}
@@ -155,7 +155,7 @@ protected static Object getAlreadyBuiltXml(Object dataSource) {
155155
}
156156
// Build the XML from string representation of the payload.
157157
String payload = MimeUtil.getMessageAsString(dataSource);
158-
return XmlUtils.parse(payload);
158+
return parseAsXml(payload);
159159
}
160160

161161
protected static void updateDataSource(BObject entityObj, Object result) {

native/src/main/java/io/ballerina/stdlib/mime/util/EntityBodyHandler.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@
2525
import io.ballerina.runtime.api.types.ArrayType;
2626
import io.ballerina.runtime.api.types.ObjectType;
2727
import io.ballerina.runtime.api.types.Type;
28-
import io.ballerina.runtime.api.utils.JsonUtils;
2928
import io.ballerina.runtime.api.utils.StringUtils;
3029
import io.ballerina.runtime.api.utils.TypeUtils;
31-
import io.ballerina.runtime.api.utils.XmlUtils;
3230
import io.ballerina.runtime.api.values.BArray;
3331
import io.ballerina.runtime.api.values.BError;
3432
import io.ballerina.runtime.api.values.BMap;
@@ -60,6 +58,10 @@
6058
import static io.ballerina.stdlib.mime.util.MimeConstants.MESSAGE_DATA_SOURCE;
6159
import static io.ballerina.stdlib.mime.util.MimeConstants.MULTIPART_AS_PRIMARY_TYPE;
6260
import static io.ballerina.stdlib.mime.util.MimeConstants.TEXT_EVENT_STREAM;
61+
import static io.ballerina.stdlib.mime.util.MimeUtil.EMPTY_JSON_DOCUMENT;
62+
import static io.ballerina.stdlib.mime.util.MimeUtil.EMPTY_XML_PAYLOAD;
63+
import static io.ballerina.stdlib.mime.util.MimeUtil.parseAsJson;
64+
import static io.ballerina.stdlib.mime.util.MimeUtil.parseAsXml;
6365

6466
/**
6567
* Entity body related operations are included here.
@@ -170,7 +172,7 @@ public static BArray constructBlobDataSource(InputStream inputStream) {
170172
public static Object constructJsonDataSource(BObject entityObj) {
171173
Channel byteChannel = getByteChannel(entityObj);
172174
if (byteChannel == null) {
173-
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, "empty JSON document");
175+
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, EMPTY_JSON_DOCUMENT);
174176
}
175177
try {
176178
return constructJsonDataSource(entityObj, byteChannel.getInputStream());
@@ -194,12 +196,12 @@ public static Object constructJsonDataSource(BObject entity, InputStream inputSt
194196
if (MimeUtil.isNotNullAndEmpty(contentTypeValue)) {
195197
String charsetValue = MimeUtil.getContentTypeParamValue(contentTypeValue, CHARSET);
196198
if (MimeUtil.isNotNullAndEmpty(charsetValue)) {
197-
jsonData = JsonUtils.parse(inputStream, charsetValue);
199+
jsonData = parseAsJson(inputStream, charsetValue);
198200
} else {
199-
jsonData = JsonUtils.parse(inputStream);
201+
jsonData = parseAsJson(inputStream);
200202
}
201203
} else {
202-
jsonData = JsonUtils.parse(inputStream);
204+
jsonData = parseAsJson(inputStream);
203205
}
204206
return jsonData;
205207
}
@@ -213,7 +215,7 @@ public static Object constructJsonDataSource(BObject entity, InputStream inputSt
213215
public static BXml constructXmlDataSource(BObject entityObj) {
214216
Channel byteChannel = getByteChannel(entityObj);
215217
if (byteChannel == null) {
216-
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, "Empty xml payload");
218+
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, EMPTY_XML_PAYLOAD);
217219
}
218220
try {
219221
return constructXmlDataSource(entityObj, byteChannel.getInputStream());
@@ -237,12 +239,12 @@ public static BXml constructXmlDataSource(BObject entityObj, InputStream inputSt
237239
if (MimeUtil.isNotNullAndEmpty(contentTypeValue)) {
238240
String charsetValue = MimeUtil.getContentTypeParamValue(contentTypeValue, CHARSET);
239241
if (MimeUtil.isNotNullAndEmpty(charsetValue)) {
240-
xmlContent = XmlUtils.parse(inputStream, charsetValue);
242+
xmlContent = parseAsXml(inputStream, charsetValue);
241243
} else {
242-
xmlContent = XmlUtils.parse(inputStream);
244+
xmlContent = parseAsXml(inputStream);
243245
}
244246
} else {
245-
xmlContent = XmlUtils.parse(inputStream);
247+
xmlContent = parseAsXml(inputStream);
246248
}
247249
return xmlContent;
248250
}

native/src/main/java/io/ballerina/stdlib/mime/util/MimeUtil.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@
2525
import io.ballerina.runtime.api.types.PredefinedTypes;
2626
import io.ballerina.runtime.api.types.Type;
2727
import io.ballerina.runtime.api.types.TypeTags;
28+
import io.ballerina.runtime.api.utils.JsonUtils;
2829
import io.ballerina.runtime.api.utils.StringUtils;
2930
import io.ballerina.runtime.api.utils.TypeUtils;
31+
import io.ballerina.runtime.api.utils.XmlUtils;
3032
import io.ballerina.runtime.api.values.BArray;
3133
import io.ballerina.runtime.api.values.BError;
3234
import io.ballerina.runtime.api.values.BMap;
3335
import io.ballerina.runtime.api.values.BObject;
3436
import io.ballerina.runtime.api.values.BStreamingJson;
3537
import io.ballerina.runtime.api.values.BString;
38+
import io.ballerina.runtime.api.values.BXml;
3639
import io.ballerina.stdlib.mime.nativeimpl.ModuleUtils;
3740
import jakarta.activation.MimeType;
3841
import jakarta.activation.MimeTypeParameterList;
@@ -44,6 +47,7 @@
4447
import java.io.IOException;
4548
import java.io.InputStream;
4649
import java.io.OutputStream;
50+
import java.nio.charset.Charset;
4751
import java.nio.charset.StandardCharsets;
4852
import java.util.Enumeration;
4953
import java.util.Random;
@@ -86,6 +90,10 @@
8690
public class MimeUtil {
8791
private static final Logger LOG = LoggerFactory.getLogger(MimeUtil.class);
8892

93+
public static final String EMPTY_JSON_DOCUMENT = "empty JSON document";
94+
public static final String EMPTY_XML_PAYLOAD = "Empty xml payload";
95+
public static final String UNEXPECTED_EOF_IN_PROLOG = "Unexpected EOF in prolog";
96+
8997
/**
9098
* Given a ballerina entity, get the content-type as a base type.
9199
*
@@ -518,6 +526,70 @@ private static boolean parseAsJson(BObject entity) {
518526
return parseAsJson != null && (boolean) entity.getNativeData(MimeConstants.PARSE_AS_JSON);
519527
}
520528

529+
public static Object parseAsJson(InputStream inputStream) {
530+
return parseAsJson(inputStream, Charset.defaultCharset().name());
531+
}
532+
533+
public static Object parseAsJson(InputStream inputStream, String charset) {
534+
try {
535+
return JsonUtils.parse(inputStream, charset);
536+
} catch (BError parserError) {
537+
handleJsonParseError(parserError);
538+
throw parserError;
539+
}
540+
}
541+
542+
public static Object parseAsJson(String jsonString) {
543+
try {
544+
return JsonUtils.parse(jsonString);
545+
} catch (BError parserError) {
546+
handleJsonParseError(parserError);
547+
throw parserError;
548+
}
549+
}
550+
551+
public static BXml parseAsXml(InputStream inputStream) {
552+
return parseAsXml(inputStream, Charset.defaultCharset().name());
553+
}
554+
555+
public static BXml parseAsXml(InputStream inputStream, String charsetValue) {
556+
try {
557+
return XmlUtils.parse(inputStream, charsetValue);
558+
} catch (BError parserError) {
559+
handleXmlParseError(parserError);
560+
throw parserError;
561+
}
562+
}
563+
564+
public static BXml parseAsXml(String xmlString) {
565+
try {
566+
return XmlUtils.parse(xmlString);
567+
} catch (BError parserError) {
568+
handleXmlParseError(parserError);
569+
throw parserError;
570+
}
571+
}
572+
573+
private static void handleJsonParseError(BError parserError) {
574+
String errorMsg = parserError.getMessage();
575+
// This EMPTY_JSON_DOCUMENT error occurs when the JSON payload is empty.
576+
// Currently, the lang lib JSON parser does not have a specific error for empty JSON payloads.
577+
// Therefore, we are checking for this specific error message to identify empty JSON payloads.
578+
if (errorMsg != null && errorMsg.startsWith(EMPTY_JSON_DOCUMENT)) {
579+
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, EMPTY_JSON_DOCUMENT, parserError);
580+
}
581+
}
582+
583+
private static void handleXmlParseError(BError parserError) {
584+
String errorMsg = parserError.getMessage();
585+
// This UNEXPECTED_EOF_IN_PROLOG error occurs when the XML payload is empty.
586+
// Currently, the lang lib XML parser does not have a specific error for empty XML payloads.
587+
// Therefore, we are checking for this specific error message to identify empty XML payloads.
588+
if (errorMsg != null && errorMsg.contains(UNEXPECTED_EOF_IN_PROLOG)) {
589+
throw MimeUtil.createError(MimeConstants.NO_CONTENT_ERROR, EMPTY_XML_PAYLOAD, parserError);
590+
}
591+
}
592+
521593
/**
522594
* Validate the given Content-Type.
523595
*

0 commit comments

Comments
 (0)