Skip to content
Closed
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
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,21 @@ or `"nsurl:NSURLErrorTimedOut"`). The `<detail>` portion is the platform's human
message — it may be OS-localized on Apple platforms and includes request specifics like
host, port, and path where the platform provides them.

Platform support: the Apple (`NSURLSession`) and Linux (`libcurl`) backends populate
these accessors today; the Windows and Android backends currently always report
empty/zero (contributions welcome — the plumbing in `UrlRequest_Base.h` is shared).
Platform support: the Apple (`NSURLSession`), Linux (`libcurl`), and Android
(`URLConnection`) backends populate these accessors. Android reports Java exceptions
as `java:JavaException(0)` and other read failures as `urllib:ResponseReadFailed(0)`.
The Windows backend currently reports empty/zero.

Android publishes the HTTP status only after the entire response body has been read.
A read exception or premature end of a known-length body clears the partial response
and reports status 0, rather than reporting the already received HTTP status as success.
Completed HTTP error responses are read from `getErrorStream()` and retain their status.

The Apple backend also checks fixed-length, unencoded HTTP bodies before publishing
the response: `NSURLSession` can report a truncated HTTP error response without an
`NSError`. Premature EOF reports `urllib:ResponseReadFailed(0)`. This check excludes
bodyless statuses and encoded/chunked bodies, whose decoded size is not the wire
`Content-Length`.

## Contributing

Expand Down
93 changes: 73 additions & 20 deletions Source/UrlRequest_Android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <android/asset_manager.h>
#include <AndroidExtensions/Globals.h>
#include <AndroidExtensions/JavaWrappers.h>
#include <algorithm>

using namespace android::global;
using namespace android::net;
Expand All @@ -15,6 +16,29 @@ namespace UrlLib
{
namespace
{
void ThrowIfFaulted(JNIEnv* env)
{
if (auto error = env->ExceptionOccurred())
{
env->ExceptionClear();
auto release = gsl::finally([&] { env->DeleteLocalRef(error); });
throw Throwable{error};
}
}

jobject GetResponseStream(URLConnection& connection, int statusCode)
{
auto env = GetEnvForCurrentThread();
// HttpURLConnection throws for HTTP error responses via getInputStream;
// their bodies are still successful transfers, exposed by getErrorStream.
auto method = env->GetMethodID(connection.GetClass(),
statusCode >= 400 ? "getErrorStream" : "getInputStream", "()Ljava/io/InputStream;");
ThrowIfFaulted(env);
auto stream = env->CallObjectMethod(connection, method);
ThrowIfFaulted(env);
return stream;
}

template<typename T> void LoadAsset(AAssetManager* assetManager, const char* url, T& data)
{
AAsset* asset = AAssetManager_open(assetManager, url, AASSET_MODE_UNKNOWN);
Expand Down Expand Up @@ -121,13 +145,10 @@ namespace UrlLib
}

connection.Connect();
int statusCode = static_cast<int>(UrlStatusCode::Ok);
if (connection.GetClass().IsAssignableFrom(HttpURLConnection::Class()))
{
m_statusCode = static_cast<UrlStatusCode>(((HttpURLConnection)connection).GetResponseCode());
}
else
{
m_statusCode = UrlStatusCode::Ok;
statusCode = ((HttpURLConnection)connection).GetResponseCode();
}

for (int n = 0;; ++n)
Expand All @@ -145,33 +166,55 @@ namespace UrlLib
m_headers.insert({lowerCaseKey, value});
}

int contentLength = connection.GetContentLength();
if (contentLength < 0)
{
contentLength = 0;
}

InputStream inputStream{connection.GetInputStream()};
ByteArrayOutputStream byteArrayOutputStream{contentLength};
const int contentLength = connection.GetContentLength();
auto env = GetEnvForCurrentThread();
auto stream = GetResponseStream(connection, statusCode);
auto releaseStream = gsl::finally([&] { env->DeleteLocalRef(stream); });
ByteArrayOutputStream byteArrayOutputStream{std::max(contentLength, 0)};
ThrowIfFaulted(env);

ByteArray byteArray{4096};
int bytesRead{};
while ((bytesRead = inputStream.Read(byteArray)) != -1)
ThrowIfFaulted(env);
size_t totalBytesRead{};
if (stream)
{
byteArrayOutputStream.Write(byteArray, 0, bytesRead);
InputStream inputStream{stream};
while (true)
{
const int bytesRead = inputStream.Read(byteArray);
// The pinned InputStream wrapper leaves read exceptions pending.
// Check before making another JNI call or interpreting its result.
ThrowIfFaulted(env);
if (bytesRead == -1)
{
break;
}
byteArrayOutputStream.Write(byteArray, 0, bytesRead);
ThrowIfFaulted(env);
totalBytesRead += static_cast<size_t>(bytesRead);
}
}
if (statusCode != 204 && statusCode != 304 &&
contentLength >= 0 && totalBytesRead != static_cast<size_t>(contentLength))
{
throw std::runtime_error{"Response body ended before Content-Length bytes were received"};
}

switch (m_responseType)
{
case UrlResponseType::String:
{
// TODO: use the charset from the content type?
m_responseString = byteArrayOutputStream.ToString("UTF-8");
auto text = byteArrayOutputStream.ToString("UTF-8");
ThrowIfFaulted(env);
m_responseString = text;
break;
}
case UrlResponseType::Buffer:
{
m_responseBuffer = byteArrayOutputStream.ToByteArray();
auto bytes = byteArrayOutputStream.ToByteArray();
ThrowIfFaulted(env);
m_responseBuffer = bytes;
break;
}
default:
Expand All @@ -182,11 +225,21 @@ namespace UrlLib

// Must happen after getting the content to get the redirected URL.
m_responseUrl = connection.GetURL().ToString();
ThrowIfFaulted(env);
m_statusCode = static_cast<UrlStatusCode>(statusCode);
}
}
catch (const Throwable&)
catch (const Throwable& error)
{
ResetForOpen();
m_responseBuffer.clear();
SetError("java", "JavaException", 0, error.what());
}
catch (const std::exception& error)
{
// Catch Java exceptions, but retain the default status code of 0 to indicate a client side error.
ResetForOpen();
m_responseBuffer.clear();
SetError("urllib", "ResponseReadFailed", 0, error.what());
}
});
}
Expand Down
23 changes: 21 additions & 2 deletions Source/UrlRequest_Apple.mm
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,35 @@ void Open(UrlMethod method, const std::string& url)
return;
}

if ([response class] == [NSHTTPURLResponse class])
if ([response isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse* httpResponse{(NSHTTPURLResponse*)response};
m_statusCode = static_cast<UrlStatusCode>(httpResponse.statusCode);

for (id key in httpResponse.allHeaderFields)
{
id value = [httpResponse.allHeaderFields objectForKey:key];
m_headers.insert({ToLower([key UTF8String]), [value UTF8String]});
}

// NSURLSession can complete truncated HTTP error bodies without an NSError.
// NSData is decoded, so only compare an identity, fixed-length body with
// Content-Length; encoded/chunked responses must use the transport's checks.
const auto encoding = m_headers.find("content-encoding");
const bool identity = encoding == m_headers.end() || encoding->second.empty() ||
ToLower(encoding->second.c_str()) == "identity";
if (httpResponse.statusCode != 204 && httpResponse.statusCode != 304 &&
identity && m_headers.find("transfer-encoding") == m_headers.end() &&
response.expectedContentLength >= 0 &&
data.length < static_cast<uint64_t>(response.expectedContentLength))
{
m_headers.clear();
SetError("urllib", "ResponseReadFailed", 0,
"Response body ended before Content-Length bytes were received");
taskCompletionSource.complete();
return;
}

m_statusCode = static_cast<UrlStatusCode>(httpResponse.statusCode);
}
else
{
Expand Down
Loading
Loading