Skip to content
Open
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
14 changes: 14 additions & 0 deletions okhttp/api/android/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public abstract interface class okhttp3/Authenticator {
public final class okhttp3/Authenticator$Companion {
}

public final class okhttp3/BinaryMode : java/lang/Enum {
public static final field FILE Lokhttp3/BinaryMode;
public static final field HEX Lokhttp3/BinaryMode;
public static final field OMIT Lokhttp3/BinaryMode;
public static final field STDIN Lokhttp3/BinaryMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lokhttp3/BinaryMode;
public static fun values ()[Lokhttp3/BinaryMode;
}

public final class okhttp3/Cache : java/io/Closeable, java/io/Flushable {
public static final field Companion Lokhttp3/Cache$Companion;
public final fun -deprecated_directory ()Ljava/io/File;
Expand Down Expand Up @@ -1018,6 +1028,10 @@ public final class okhttp3/Request {
public final fun tag ()Ljava/lang/Object;
public final fun tag (Ljava/lang/Class;)Ljava/lang/Object;
public final fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object;
public final fun toCurl ()Ljava/lang/String;
public final fun toCurl (Lokhttp3/BinaryMode;)Ljava/lang/String;
public final fun toCurl (Lokhttp3/BinaryMode;Ljava/lang/String;)Ljava/lang/String;
public static synthetic fun toCurl$default (Lokhttp3/Request;Lokhttp3/BinaryMode;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
public fun toString ()Ljava/lang/String;
public final fun url ()Lokhttp3/HttpUrl;
}
Expand Down
14 changes: 14 additions & 0 deletions okhttp/api/jvm/okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public abstract interface class okhttp3/Authenticator {
public final class okhttp3/Authenticator$Companion {
}

public final class okhttp3/BinaryMode : java/lang/Enum {
public static final field FILE Lokhttp3/BinaryMode;
public static final field HEX Lokhttp3/BinaryMode;
public static final field OMIT Lokhttp3/BinaryMode;
public static final field STDIN Lokhttp3/BinaryMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lokhttp3/BinaryMode;
public static fun values ()[Lokhttp3/BinaryMode;
}

public final class okhttp3/Cache : java/io/Closeable, java/io/Flushable {
public static final field Companion Lokhttp3/Cache$Companion;
public final fun -deprecated_directory ()Ljava/io/File;
Expand Down Expand Up @@ -1017,6 +1027,10 @@ public final class okhttp3/Request {
public final fun tag ()Ljava/lang/Object;
public final fun tag (Ljava/lang/Class;)Ljava/lang/Object;
public final fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object;
public final fun toCurl ()Ljava/lang/String;
public final fun toCurl (Lokhttp3/BinaryMode;)Ljava/lang/String;
public final fun toCurl (Lokhttp3/BinaryMode;Ljava/lang/String;)Ljava/lang/String;
public static synthetic fun toCurl$default (Lokhttp3/Request;Lokhttp3/BinaryMode;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
public fun toString ()Ljava/lang/String;
public final fun url ()Lokhttp3/HttpUrl;
}
Expand Down
8 changes: 8 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/BinaryMode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package okhttp3

enum class BinaryMode {
HEX, // hex encode
OMIT, // "[binary body omitted]"
FILE, // --data-binary @filename
STDIN, // --data-binary @-
}
104 changes: 104 additions & 0 deletions okhttp/src/commonJvmAndroid/kotlin/okhttp3/Request.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
package okhttp3

import java.net.URL
import java.nio.charset.StandardCharsets
import kotlin.reflect.KClass
import kotlin.reflect.cast
import okhttp3.Headers.Companion.headersOf
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.internal.http.GzipRequestBody
import okhttp3.internal.http.HttpMethod
import okhttp3.internal.isSensitiveHeader
import okio.Buffer

/**
* An HTTP request. Instances of this class are immutable if their [body] is null or itself
Expand Down Expand Up @@ -430,4 +432,106 @@ class Request internal constructor(

open fun build(): Request = Request(this)
}

/**
* Returns a cURL command equivalent to this request, useful for debugging and reproducing requests.
*
* This includes the HTTP method, headers, request body (if present), and URL.
*
* Example:
* ```
* curl -X POST -H "Authorization: Bearer token" --data "{\"key\":\"value\"}" "https://example.com/api"
* ```
*
* **Note:** This method will write the body
* to a temporary [okio.Buffer] in memory. This may have side effects if the [RequestBody] is streaming
* or can be consumed only once. Calling this method might prevent re-sending the request body later.
*
* @param binaryFileName default file name to use when dumping binary body data to a file (default: `"request_body.bin"`)
* @param binaryMode default mode to use when writing binary body data (default: `"BinaryMode.STDIN"`)
* @return a cURL command string representing this request.
*/
@JvmOverloads
fun toCurl(
binaryMode: BinaryMode = BinaryMode.STDIN,
binaryFileName: String? = "request_body.bin",
): String {
val curl = StringBuilder("curl")

// Add method if not GET
if (method != "GET") {
curl.append(" -X ").append(method)
}

// Append headers
for ((name, value) in headers) {
curl
.append(" -H \"")
.append(name)
.append(": ")
.append(value)
.append("\"")
}

// Append body if present
body?.let { requestBody ->
val buffer = Buffer()
requestBody.writeTo(buffer)

// Clone so we can read multiple times without consuming
val peekBuffer = buffer.clone()
val isBinary = isBinaryData(peekBuffer)

if (isBinary) {
when (binaryMode) {
BinaryMode.HEX -> {
curl.append(" --data-binary \"")
val hexBuffer = buffer.clone()
while (!hexBuffer.exhausted()) {
val b = hexBuffer.readByte().toInt() and 0xFF
curl.append("%02x".format(b))
}
curl.append("\"")
}
BinaryMode.FILE -> {
curl.append(" --data-binary @").append(binaryFileName)
}
BinaryMode.STDIN -> {
curl.append(" --data-binary @-")
}
BinaryMode.OMIT -> {
curl.append(" --data-binary \"[binary body omitted]\"")
}
}
} else {
val bodyString = buffer.readString(StandardCharsets.UTF_8)
curl
.append(" --data \"")
.append(bodyString.replace("\"", "\\\""))
.append("\"")
}
}

curl.append(" \"").append(url).append("\"")
return curl.toString()
}

/**
* Detects binary data by checking for non-printable characters in a buffer.
*/
private fun isBinaryData(peekBuffer: Buffer): Boolean {
var totalBytes = 0
var binaryCount = 0
val textSafeBytes = intArrayOf(0x09, 0x0A, 0x0D) // tab, LF, CR

while (!peekBuffer.exhausted() && totalBytes < 4096) { // limit to first 4KB for performance
val b = peekBuffer.readByte().toInt() and 0xFF
if ((b < 0x20 && b !in textSafeBytes) || b > 0x7E) {
binaryCount++
}
totalBytes++
}

return totalBytes > 0 && binaryCount > totalBytes * 0.1
}
}
163 changes: 163 additions & 0 deletions okhttp/src/jvmTest/kotlin/okhttp3/RequestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,169 @@ class RequestTest {
}
}

@Test
fun curlGet() {
val request =
Request
.Builder()
.url("https://example.com")
.header("Authorization", "Bearer abc123")
.build()

val curl = request.toCurl()
assertThat(curl)
.isEqualTo("curl -H \"Authorization: Bearer abc123\" \"https://example.com/\"")
}

@Test
fun curlPostWithBody() {
val mediaType = "application/json".toMediaType()
val body = "{\"key\":\"value\"}".toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/data")
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer abc123")
.build()

val curl = request.toCurl()
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer abc123\" --data \"{\\\"key\\\":\\\"value\\\"}\" \"https://api.example.com/data\"",
)
}

@Test
fun curlPostWithComplexBody() {
val mediaType = "application/json".toMediaType()
val jsonBody =
"""
{
"user": {
"id": 123,
"name": "John Doe"
},
"roles": ["admin", "editor"],
"active": true
}
""".trimIndent()

val body = jsonBody.toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/users")
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer xyz789")
.build()

val curl = request.toCurl()
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer xyz789\" --data \"{\n" +
" \\\"user\\\": {\n" +
" \\\"id\\\": 123,\n" +
" \\\"name\\\": \\\"John Doe\\\"\n" +
" },\n" +
" \\\"roles\\\": [\\\"admin\\\", \\\"editor\\\"],\n" +
" \\\"active\\\": true\n" +
"}\" \"https://api.example.com/users\"",
)
}

@Test
fun curlPostWithBinaryBody_DefaultSTDIN() {
val mediaType = "application/octet-stream".toMediaType()
val binaryData = byteArrayOf(0x00, 0x01, 0x02, 0x03)

val body = binaryData.toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/upload")
.post(body)
.addHeader("Content-Type", "application/octet-stream")
.build()

val curl = request.toCurl() // default is BinaryMode.STDIN
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/octet-stream\" --data-binary @- \"https://api.example.com/upload\"",
)
}

@Test
fun curlPostWithBinaryBody_HexMode() {
val mediaType = "application/octet-stream".toMediaType()
val binaryData = byteArrayOf(0x00, 0x01, 0x02, 0x03)

val body = binaryData.toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/upload")
.post(body)
.addHeader("Content-Type", "application/octet-stream")
.build()

val curl = request.toCurl(BinaryMode.HEX)
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/octet-stream\" --data-binary \"00010203\" \"https://api.example.com/upload\"",
)
}

@Test
fun curlPostWithBinaryBody_FileMode() {
val mediaType = "application/octet-stream".toMediaType()
val binaryData = byteArrayOf(0xAA.toByte(), 0xBB.toByte())

val body = binaryData.toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/upload")
.post(body)
.addHeader("Content-Type", "application/octet-stream")
.build()

val curl = request.toCurl(BinaryMode.FILE, "mydata.bin")
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/octet-stream\" --data-binary @mydata.bin \"https://api.example.com/upload\"",
)
}

@Test
fun curlPostWithBinaryBody_OmitMode() {
val mediaType = "application/octet-stream".toMediaType()
val binaryData = byteArrayOf(0x10, 0x20)

val body = binaryData.toRequestBody(mediaType)

val request =
Request
.Builder()
.url("https://api.example.com/upload")
.post(body)
.addHeader("Content-Type", "application/octet-stream")
.build()

val curl = request.toCurl(BinaryMode.OMIT)
assertThat(curl)
.isEqualTo(
"curl -X POST -H \"Content-Type: application/octet-stream\" --data-binary \"[binary body omitted]\" \"https://api.example.com/upload\"",
)
}

private fun bodyToHex(body: RequestBody): String {
val buffer = Buffer()
body.writeTo(buffer)
Expand Down