Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Concurrency tests #129

Merged
merged 6 commits into from

2 participants

@dkowis

So this just puts up some concurrent execution tests. Guaranteeing that Betamax is able to serve concurrent requests nicely.

It also flushed out a problem with the content-length header being set to something such that it's not calculated again when an individual modifies the response, but that's a different bug.

dkowis added some commits
@dkowis dkowis Created a failing test
So it fails playback when doing things concurrently using the
AsyncHttpClient from Apache Http Components.

This is similar to the behavior I found when I ran the proxy standalone.
Of note is that it doesn't seem to fail if the tape is READ_WRITE. Will
have to add a test to demonstrate that as well, then refactor all the
duplicated code out.
6f1e717
@dkowis dkowis Modified the test to do both tape modes
Now it's a bit more configurable, and no repeat code. yay
eea748f
@dkowis dkowis Adding deadlocked thread count 4022d7e
@dkowis dkowis Seems I probably made a mistake here. That error message is wrong 86e1b80
@dkowis dkowis a small change or two to the BetamaxFilters
Mostly a TODO note with regards to how to handle the CONNECT part.

And then using a ProxyUtils method that was mentioned in the
littleproxy javadocs instead of doing the same check ourselves. It reads
a bit nicer.
194bd6b
@dkowis dkowis Updating the concurrency tests to prove they work 969ee01
@robfletcher robfletcher merged commit 8ac02ec into robfletcher:master
@robfletcher
Owner

Awesome, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 27, 2014
  1. @dkowis

    Created a failing test

    dkowis authored dkowis committed
    So it fails playback when doing things concurrently using the
    AsyncHttpClient from Apache Http Components.
    
    This is similar to the behavior I found when I ran the proxy standalone.
    Of note is that it doesn't seem to fail if the tape is READ_WRITE. Will
    have to add a test to demonstrate that as well, then refactor all the
    duplicated code out.
  2. @dkowis

    Modified the test to do both tape modes

    dkowis authored dkowis committed
    Now it's a bit more configurable, and no repeat code. yay
  3. @dkowis

    Adding deadlocked thread count

    dkowis authored dkowis committed
  4. @dkowis
Commits on Jan 28, 2014
  1. @dkowis

    a small change or two to the BetamaxFilters

    dkowis authored
    Mostly a TODO note with regards to how to handle the CONNECT part.
    
    And then using a ProxyUtils method that was mentioned in the
    littleproxy javadocs instead of doing the same check ourselves. It reads
    a bit nicer.
  2. @dkowis
This page is out of date. Refresh to see the latest.
View
20 betamax-proxy/src/main/java/co/freeside/betamax/proxy/BetamaxFilters.java
@@ -29,6 +29,8 @@
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.*;
import org.littleshoot.proxy.HttpFiltersAdapter;
+import org.littleshoot.proxy.impl.ProxyUtils;
+
import static co.freeside.betamax.Headers.*;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
@@ -55,6 +57,12 @@ public HttpResponse requestPre(HttpObject httpObject) {
try {
HttpResponse response = null;
if (httpObject instanceof HttpRequest) {
+ //TODO: I believe this is where the CONNECT needs to be caught...
+ // This would require changing the predicate to include all things
+ // As well, an appropriate response that the connect succeeded would have to be returned
+ // But only if the server we are trying to connect to actually has an entry in the tape
+ // It's something of a race condition with the SSL stuff. Because I don't believe we get a path
+ // When we have the connect go through. Could we send the connect later if we didn't send it now?
request.copyHeaders((HttpMessage) httpObject);
}
@@ -64,7 +72,7 @@ public HttpResponse requestPre(HttpObject httpObject) {
}
// If it's the last one, we want to take further steps, like checking to see if we've recorded on it!
- if (httpObject instanceof LastHttpContent) {
+ if (ProxyUtils.isLastChunk(httpObject)) {
// We will have collected the last of the http Request finally
// And now we're ready to intercept it and do proxy-type-things
response = onRequestIntercepted().orNull();
@@ -82,7 +90,11 @@ public HttpResponse requestPost(HttpObject httpObject) {
setViaHeader((HttpMessage) httpObject);
}
- return null;
+ if(httpObject instanceof HttpResponse) {
+ return (HttpResponse) httpObject;
+ } else {
+ return null;
+ }
}
@Override
@@ -100,7 +112,7 @@ public HttpObject responsePre(HttpObject httpObject) {
}
}
- if (httpObject instanceof LastHttpContent) {
+ if (ProxyUtils.isLastChunk(httpObject)) {
if (tape.isWritable()) {
LOG.info(String.format("Recording to tape %s", tape.getName()));
tape.record(request, upstreamResponse);
@@ -118,7 +130,7 @@ public HttpObject responsePost(HttpObject httpObject) {
setBetamaxHeader((HttpResponse) httpObject, "REC");
setViaHeader((HttpMessage) httpObject);
}
-
+
return httpObject;
}
View
2  betamax-proxy/src/main/java/co/freeside/betamax/proxy/netty/NettyRequestAdapter.java
@@ -27,7 +27,7 @@ public static NettyRequestAdapter wrap(HttpObject message) {
if (message instanceof HttpRequest) {
return new NettyRequestAdapter((HttpRequest) message);
} else {
- throw new IllegalArgumentException(String.format("%s is not an instance of %s", message.getClass().getName(), FullHttpRequest.class.getName()));
+ throw new IllegalArgumentException(String.format("%s is not an instance of %s", message.getClass().getName(), HttpRequest.class.getName()));
}
}
View
170 betamax-proxy/src/test/groovy/co/freeside/betamax/proxy/concurrency/ConcurrencyTest.groovy
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package co.freeside.betamax.proxy.concurrency
+
+import co.freeside.betamax.ProxyConfiguration
+import co.freeside.betamax.Recorder
+import groovy.json.JsonSlurper
+import org.apache.http.HttpHost
+import org.apache.http.HttpResponse
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.concurrent.FutureCallback
+import org.apache.http.entity.ContentType
+import org.apache.http.entity.StringEntity
+import org.apache.http.impl.nio.client.HttpAsyncClients
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.lang.management.ManagementFactory
+import java.lang.management.ThreadMXBean
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+import static co.freeside.betamax.TapeMode.READ_ONLY
+import static co.freeside.betamax.TapeMode.READ_WRITE
+
+/**
+ * Doing some concurrent execution against betamax trying to flush out why things don't always behave the way I
+ * expected them to.
+ *
+ * Turns out there needs to be some kind of warning for the Content-Length header. If it's not precise,
+ * The Apache HttpAsyncClient will continue to wait for the remainder of the content, blocking littleproxy from
+ * Serving any other requests, because it has no other content to deliver. Oops!
+ */
+@Unroll
+class ConcurrencyTest extends Specification {
+
+ @Shared
+ def tapeRoot = new File(ConcurrencyTest.class.getResource("/betamax/tapes/").toURI())
+
+ /**
+ * http://meteatamel.wordpress.com/2012/03/21/deadlock-detection-in-java/
+ * handy
+ */
+ def countDeadlockedThreads() {
+ ThreadMXBean threadBean = ManagementFactory.getThreadMXBean()
+ long[] threadIds = threadBean.findDeadlockedThreads()
+ int deadlockedThreads = threadIds != null ? threadIds.length : 0
+ println("Number of deadlocked threads: $deadlockedThreads")
+ }
+
+ void "#amount Concurrent accesses to a #tapeMode tape works"() {
+ given:
+ def proxyConfig = ProxyConfiguration.builder()
+ .tapeRoot(tapeRoot)
+ .defaultMode(tapeMode)
+ .defaultMatchRule(new PostingMatchRule())
+ .build()
+
+ def recorder = new Recorder(proxyConfig)
+ recorder.start("concurrentTape")
+
+ def builder = HttpAsyncClients.custom()
+ builder.setProxy(new HttpHost("127.0.0.1", 5555))
+
+ def httpClient = builder.build()
+
+ httpClient.start()
+ def requestCount = amount
+ println("client started, request count ${requestCount}")
+
+ List<HttpPost> requests = []
+
+ print("\t")
+ requestCount.times { num ->
+ def post = new HttpPost("http://httpbin.org/post")
+ post.setEntity(new StringEntity(num.toString(), ContentType.TEXT_PLAIN))
+ print(".")
+ requests.add post
+ }
+ println()
+
+ when:
+ //Map the request body to the result body, they should match
+ def resultsMap = new ConcurrentHashMap<String, String>()
+
+ def latch = new CountDownLatch(requestCount)
+ println("Executing $requestCount requests concurrently")
+ requests.each { request ->
+ httpClient.execute(request, new FutureCallback<HttpResponse>() {
+ @Override
+ void completed(HttpResponse result) {
+ //HTTPBin will return what we post, so we'll just make sure that we save
+ // the request's body and the response body together for this result
+ // that way we can ensure that we're not clobbering stuff, since each request will have a different
+ // request body
+ //Using this to get the "data" attribute out
+ //Doing this first, because it might actually bomb, heh
+ latch.countDown()
+ try {
+ def json = new JsonSlurper().parseText(result.entity.content.text)
+
+ resultsMap.put(request.entity.content.text, json.data)
+ } catch (e) {
+ resultsMap.put(request.entity.content.text, "FAILED: ${e.message}")
+ }
+ }
+
+ @Override
+ void failed(Exception ex) {
+ resultsMap.put(request.entity.content.text, "HTTP_FAILED: ${ex.message}")
+ latch.countDown()
+ }
+
+ @Override
+ void cancelled() {
+ resultsMap.put(request.entity.content.text, "CANCELLED!")
+ latch.countDown()
+ }
+ })
+ }
+
+ then:
+ //This should happen fast
+ println("Awaiting request completion with latch")
+ latch.await(5, TimeUnit.SECONDS)
+
+ while(resultsMap.size() != requestCount) {
+ //Wait until the map lines up with the countdown latch
+ Thread.sleep(10)
+ }
+
+ //We'll verify it just for sanity
+ resultsMap.size() == requestCount
+
+ println("Verifying results map")
+ resultsMap.each { pair ->
+ //This assertion doesn't seem to work?
+ assert (pair.key == pair.value)
+ }
+
+ cleanup:
+ println("counting deadlocked threads")
+ countDeadlockedThreads()
+
+ println("stopping recorder")
+ recorder?.stop()
+ println("Closing httpclient")
+ httpClient?.close()
+
+ where:
+ tapeMode << [READ_ONLY, READ_WRITE]
+ amount << [10, 8]
+ }
+}
View
41 betamax-proxy/src/test/groovy/co/freeside/betamax/proxy/concurrency/PostingMatchRule.groovy
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package co.freeside.betamax.proxy.concurrency
+
+import co.freeside.betamax.MatchRule
+import co.freeside.betamax.message.Request
+
+import java.util.concurrent.atomic.AtomicInteger
+
+class PostingMatchRule implements MatchRule {
+
+ @Override
+ boolean isMatch(Request a, Request b) {
+ if (a.uri == b.uri && a.method == b.method) {
+ //Same method and URI, lets do a body comparison
+ //Can only consume the body once, once it's gone it's gone.
+ def aBody = a.bodyAsText.input.text
+ def bBody = b.bodyAsText.input.text
+
+ //Right now, lets just compare the bodies also
+ return aBody == bBody
+ } else {
+ //URI and method don't match, so we're going to bail
+ return false
+ }
+ }
+}
View
383 betamax-proxy/src/test/resources/betamax/tapes/concurrentTape.yaml
@@ -0,0 +1,383 @@
+!tape
+name: concurrentTape
+interactions:
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '0'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "0",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '1'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "1",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '2'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "2",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '3'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "3",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '4'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "4",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '5'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "5",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '6'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "6",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '7'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "7",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '8'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "8",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
+- recorded: 2014-01-27T01:03:19.972Z
+ request:
+ method: POST
+ uri: http://httpbin.org/post
+ headers:
+ Content-Length: '1'
+ Content-Type: text/plain; charset=ISO-8859-1
+ Host: httpbin.org
+ Proxy-Connection: Keep-Alive
+ User-Agent: Apache-HttpAsyncClient/4.0 (java 1.5)
+ body: '9'
+ response:
+ status: 200
+ headers:
+ Access-Control-Allow-Origin: '*'
+ Connection: keep-alive
+ Content-Type: application/json
+ Date: Mon, 27 Jan 2014 01:03:19 GMT
+ Server: gunicorn/0.17.4
+ body: |-
+ {
+ "args": {},
+ "url": "http://httpbin.org/post",
+ "files": {},
+ "headers": {
+ "X-Request-Id": "ce99c82f-c429-4627-96ee-b394409a03b8",
+ "Host": "httpbin.org",
+ "User-Agent": "Apache-HttpAsyncClient/4.0 (java 1.5)",
+ "Content-Type": "text/plain; charset=ISO-8859-1",
+ "Connection": "close",
+ "Via": "Betamax",
+ "Content-Length": "1"
+ },
+ "data": "9",
+ "json": 0,
+ "origin": "173.175.114.180",
+ "form": {}
+ }
Something went wrong with that request. Please try again.