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

Alter HTTP requests to stop using chunked transfer encoding #1077

Merged
merged 40 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d0b3b50
Alter HTTP requests to stop using chunked transfer encoding
fractalwrench Jan 13, 2021
a96ba7d
Remove all soft fails
twometresteve Jan 15, 2021
40e6250
Remove Android 8.0 - 8.1 is sufficient if passing
twometresteve Jan 15, 2021
8bf35e5
Reinstate skipped Android 8.1 temporarily
twometresteve Jan 15, 2021
ddf8712
Allow for full scheduled builds
twometresteve Jan 15, 2021
6fc7114
Flush out genuine flakes
twometresteve Jan 15, 2021
2ce135c
Use resilient driver
twometresteve Jan 16, 2021
acc3832
Reinstate @Flaky tags for appropriate scenarios
twometresteve Jan 16, 2021
4095227
Remove Flaky marker where no longer believe to be the case
twometresteve Jan 17, 2021
f4d6501
Redundant feature removed
twometresteve Jan 18, 2021
966d8ce
Removed comment
twometresteve Jan 18, 2021
5caac07
Run Flaky tests separately
twometresteve Jan 19, 2021
ed4bc9a
Expect breadcrumb that really should always be there.
twometresteve Jan 19, 2021
cb993d0
Merge branch 'next' into PLAT-5698/chunked-transfer-encoding
fractalwrench Jan 20, 2021
b57f3f4
Merge pull request #1081 from bugsnag/tms/review-flake-tags
fractalwrench Jan 20, 2021
3dfd556
Use in-development Maze Runner
twometresteve Jan 15, 2021
0a089db
Migrate to Maze.driver
twometresteve Jan 15, 2021
928ca87
Migrate to Maze.config
twometresteve Jan 15, 2021
9f2c3ad
Send to separate endpoints
twometresteve Jan 15, 2021
b70df7b
Use Maze namespace
twometresteve Jan 15, 2021
0b13515
Update “the payload field” to error across the board
twometresteve Jan 15, 2021
d70ccfd
Rename “I wait to receive a request” everywhere
twometresteve Jan 15, 2021
7d94773
Use MR docker image
twometresteve Jan 15, 2021
4cc8b9f
Fix test scenarios
twometresteve Jan 15, 2021
80bf42e
Rename steps for consistency
twometresteve Jan 15, 2021
7165cb4
Correct choice of step
twometresteve Jan 15, 2021
35db7b6
Absorb step into MazeRunner
twometresteve Jan 15, 2021
75384c7
Fix smoke tests
twometresteve Jan 15, 2021
532d386
Move steps into maze runner
twometresteve Jan 15, 2021
e3bf168
Use latest MR development branch
twometresteve Jan 16, 2021
bbe1700
Qualify Maze calls
twometresteve Jan 16, 2021
fe51ac8
Updates for Maze v4
twometresteve Jan 16, 2021
d136711
Use session specific step
twometresteve Jan 17, 2021
e3a3538
Correct request type expected
twometresteve Jan 17, 2021
1e65fbe
Merge pull request #1080 from bugsnag/tms/maze-v4
fractalwrench Jan 20, 2021
a956495
ci: add missing tags from mazerunner buildkite yaml
fractalwrench Jan 20, 2021
62c34d9
Various updates for flaking scenarios
twometresteve Jan 20, 2021
4b28a20
test: reinstate flaky test flag for failing test
fractalwrench Jan 20, 2021
d50d83a
Merge pull request #1082 from bugsnag/tms/further-flake-updates
fractalwrench Jan 20, 2021
29a67fc
ci: fix yaml
fractalwrench Jan 20, 2021
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## TBD

* Alter HTTP requests to stop using chunked transfer encoding
[#1077](https://github.com/bugsnag/bugsnag-android/pull/1077)

## 5.5.0 (2021-01-07)

This release supports initializing Bugsnag in multi processes apps. If your app uses Bugsnag in multiple processes, you should initialize Bugsnag
Expand Down
1 change: 1 addition & 0 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun updateSeverityInternal(severity: Severity)</ID>
<ID>ProtectedMemberInFinalClass:PluginClient.kt$PluginClient$protected val plugins: Set&lt;Plugin&gt;</ID>
<ID>ReturnCount:DefaultDelivery.kt$DefaultDelivery$fun deliver( urlString: String, streamable: JsonStream.Streamable, headers: Map&lt;String, String?&gt; ): DeliveryStatus</ID>
<ID>VariableNaming:EventInternal.kt$EventInternal$/** * @return user information associated with this Event */ internal var _user = User(null, null, null)</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package com.bugsnag.android

import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL

internal class DefaultDelivery(private val connectivity: Connectivity?, val logger: Logger) : Delivery {
/**
* Converts a [JsonStream.Streamable] into JSON, placing it in a [ByteArray]
*/
internal fun serializeJsonPayload(streamable: JsonStream.Streamable): ByteArray {
return ByteArrayOutputStream().use { baos ->
JsonStream(PrintWriter(baos).buffered()).use(streamable::toStream)
baos.toByteArray()
}
}

internal class DefaultDelivery(
private val connectivity: Connectivity?,
val logger: Logger
) : Delivery {

override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
val status = deliver(deliveryParams.endpoint, payload, deliveryParams.headers)
Expand All @@ -33,36 +48,58 @@ internal class DefaultDelivery(private val connectivity: Connectivity?, val logg
var conn: HttpURLConnection? = null

try {
val url = URL(urlString)
conn = url.openConnection() as HttpURLConnection
conn.doOutput = true
conn.setChunkedStreamingMode(0)
conn.addRequestProperty("Content-Type", "application/json")

headers.forEach { (key, value) ->
if (value != null) {
conn.addRequestProperty(key, value)
}
}

conn.outputStream.bufferedWriter().use { writer ->
streamable.toStream(JsonStream(writer))
}
val json = serializeJsonPayload(streamable)
conn = makeRequest(URL(urlString), json, headers)

// End the request, get the response code
val responseCode = conn.responseCode
val status = getDeliveryStatus(responseCode)
logRequestInfo(responseCode, conn, status)
return status
} catch (oom: OutOfMemoryError) {
// attempt to persist the payload on disk. This approach uses streams to write to a
// file, which takes less memory than serializing the payload into a ByteArray, and
// therefore has a reasonable chance of retaining the payload for future delivery.
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
return DeliveryStatus.UNDELIVERED
} catch (exception: IOException) {
logger.w("IOException encountered in request", exception)
return DeliveryStatus.UNDELIVERED
} catch (exception: Exception) {
logger.w("Unexpected error delivering payload", exception)
return DeliveryStatus.FAILURE
} finally {
IOUtils.close(conn)
conn?.disconnect()
}
}

private fun makeRequest(
url: URL,
json: ByteArray,
headers: Map<String, String?>
): HttpURLConnection {
val conn = url.openConnection() as HttpURLConnection
conn.doOutput = true

// avoids creating a buffer within HttpUrlConnection, see
// https://developer.android.com/reference/java/net/HttpURLConnection
conn.setFixedLengthStreamingMode(json.size)

// calculate the SHA-1 digest and add all other headers
computeSha1Digest(json)?.let { digest ->
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
}
headers.forEach { (key, value) ->
if (value != null) {
conn.addRequestProperty(key, value)
}
}

// write the JSON payload
conn.outputStream.use {
it.write(json)
}
return conn
}

private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
Expand All @@ -72,9 +109,14 @@ internal class DefaultDelivery(private val connectivity: Connectivity?, val logg
"headers: ${conn.headerFields}"
)

conn.inputStream.bufferedReader().use {
logger.d("Received request response: ${it.readText()}")
}

if (status != DeliveryStatus.DELIVERED) {
val errBody = conn.errorStream.bufferedReader().readText()
logger.w("Request error details: $errBody")
conn.errorStream.bufferedReader().use {
logger.w("Request error details: ${it.readText()}")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import java.util.Date

private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version"
private const val HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At"
private const val HEADER_BUGSNAG_INTEGRITY = "Bugsnag-Integrity"
private const val HEADER_CONTENT_TYPE = "Content-Type"
internal const val HEADER_BUGSNAG_INTEGRITY = "Bugsnag-Integrity"
internal const val HEADER_API_KEY = "Bugsnag-Api-Key"
internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"

Expand All @@ -20,8 +21,8 @@ internal fun errorApiHeaders(payload: EventPayload): Map<String, String?> {
return mapOf(
HEADER_API_PAYLOAD_VERSION to "4.0",
HEADER_API_KEY to payload.apiKey,
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date()),
HEADER_BUGSNAG_INTEGRITY to computeSha1Digest(payload)
HEADER_CONTENT_TYPE to "application/json",
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
)
}

Expand All @@ -30,22 +31,22 @@ internal fun errorApiHeaders(payload: EventPayload): Map<String, String?> {
*
* @return the HTTP headers
*/
internal fun sessionApiHeaders(apiKey: String, payload: Session): Map<String, String?> = mapOf(
internal fun sessionApiHeaders(apiKey: String): Map<String, String?> = mapOf(
HEADER_API_PAYLOAD_VERSION to "1.0",
HEADER_API_KEY to apiKey,
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date()),
HEADER_BUGSNAG_INTEGRITY to computeSha1Digest(payload)
HEADER_CONTENT_TYPE to "application/json",
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
)

internal fun computeSha1Digest(payload: JsonStream.Streamable): String? {
internal fun computeSha1Digest(payload: ByteArray): String? {
runCatching {
val shaDigest = MessageDigest.getInstance("SHA-1")
val builder = StringBuilder("sha1 ")

// Pipe the object through a no-op output stream
DigestOutputStream(NullOutputStream(), shaDigest).use { stream ->
stream.bufferedWriter().use { writer ->
payload.toStream(JsonStream(writer))
stream.buffered().use { writer ->
writer.write(payload)
}
shaDigest.digest().forEach { byte ->
builder.append(String.format("%02x", byte))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ static void closeQuietly(@Nullable final Closeable closeable) {
}
}

static void close(@Nullable final URLConnection conn) {
if (conn instanceof HttpURLConnection) {
((HttpURLConnection) conn).disconnect();
}
}

static int copy(@NonNull final Reader input,
@NonNull final Writer output) throws IOException {
char[] buffer = new char[DEFAULT_BUFFER_SIZE];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ internal data class ImmutableConfig(
DeliveryParams(endpoints.notify, errorApiHeaders(payload))

@JvmName("getSessionApiDeliveryParams")
internal fun getSessionApiDeliveryParams(session: Session) =
DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey, session))
internal fun getSessionApiDeliveryParams() =
DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey))
}

internal fun convertToImmutableConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ void flushStoredSession(File storedFile) {
}

DeliveryStatus deliverSessionPayload(Session payload) {
DeliveryParams params = configuration.getSessionApiDeliveryParams(payload);
DeliveryParams params = configuration.getSessionApiDeliveryParams();
Delivery delivery = configuration.getDelivery();
return delivery.deliver(payload, params);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.bugsnag.android

import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import java.io.BufferedWriter
import java.io.StringWriter
import java.util.Date

class DeliveryHeadersTest {
Expand All @@ -15,8 +15,10 @@ class DeliveryHeadersTest {
@Test
fun computeSha1Digest() {
val payload = BugsnagTestUtils.generateEventPayload(generateImmutableConfig())
val firstSha = requireNotNull(computeSha1Digest(payload))
val secondSha = requireNotNull(computeSha1Digest(payload))
val payload1 = serializeJsonPayload(payload)
val firstSha = requireNotNull(computeSha1Digest(payload1))
val payload2 = serializeJsonPayload(payload)
val secondSha = requireNotNull(computeSha1Digest(payload2))

// the hash equals the expected value
assertTrue(firstSha.matches(sha1Regex))
Expand All @@ -26,7 +28,8 @@ class DeliveryHeadersTest {

// altering the streamable alters the hash
payload.event!!.device.id = "50923"
val differentSha = requireNotNull(computeSha1Digest(payload))
val payload3 = serializeJsonPayload(payload)
val differentSha = requireNotNull(computeSha1Digest(payload3))
assertNotEquals(firstSha, differentSha)
assertTrue(differentSha.matches(sha1Regex))
}
Expand All @@ -37,24 +40,18 @@ class DeliveryHeadersTest {
val payload = BugsnagTestUtils.generateEventPayload(config)
val headers = config.getErrorApiDeliveryParams(payload).headers
assertEquals(config.apiKey, headers["Bugsnag-Api-Key"])
Assert.assertNotNull(headers["Bugsnag-Sent-At"])
Assert.assertNotNull(headers["Bugsnag-Payload-Version"])

val integrity = requireNotNull(headers["Bugsnag-Integrity"])
assertTrue(integrity.matches(sha1Regex))
assertEquals("application/json", headers["Content-Type"])
assertNotNull(headers["Bugsnag-Sent-At"])
assertNotNull(headers["Bugsnag-Payload-Version"])
}

@Test
fun verifySessionApiHeaders() {
val config = generateImmutableConfig()
val user = User("123", "hi@foo.com", "Li")
val session = Session("abc", Date(0), user, 1, 0, Notifier(), NoopLogger)
val headers = config.getSessionApiDeliveryParams(session).headers
val headers = config.getSessionApiDeliveryParams().headers
assertEquals(config.apiKey, headers["Bugsnag-Api-Key"])
Assert.assertNotNull(headers["Bugsnag-Sent-At"])
Assert.assertNotNull(headers["Bugsnag-Payload-Version"])

val integrity = requireNotNull(headers["Bugsnag-Integrity"])
assertTrue(integrity.matches(sha1Regex))
assertEquals("application/json", headers["Content-Type"])
assertNotNull(headers["Bugsnag-Sent-At"])
assertNotNull(headers["Bugsnag-Payload-Version"])
}
}