Fork me on GitHub

Back to Home

HTTP endpoint configuration HOWTO

Table of contents

Summary

This page explains the usage, intent and behavior of each property on the request and response objects.

Also, you will learn about request proxying to other hosts, regex stubbing for dynamic matching, stubbing HTTP 30x redirects, record-and-replay and more.

Click to expand

Here is a fully-populated, unrealistic endpoint:

-  description: Optional description shown in logs
   uuid: fdkfsd8f8ds7f
   request:
      url: ^/your/awesome/endpoint$
      method: POST
      query:
         exclamation: post requests can have query strings!
      headers:
         content-type: application/xml
      post: >
         <!xml blah="blah blah blah">
         <envelope>
            <unaryTag/>
         </envelope>
      file: tryMyFirst.xml

   response:
      status: 200
      latency: 5000
      headers:
         content-type: application/xml
         server: stubbedServer/4.2
      body: >
         <!xml blah="blah blah blah">
         <responseXML>
            <content></content>
         </responseXML>
      file: responseData.xml

Keep on reading to understand how to add http endpoint configurations to your stubby4j YAML config.

Back to top

Stub/Feature

description (optional)

-  description: Stub one
   request:
      url: ^/one$
      method: GET

   response:
      status: 200
      latency: 100
      body: 'One!'

-  description: Stub two
   request:
      url: ^/two$
      method: GET

   response:
      status: 200
      latency: 100
      body: 'Two!'

-  request:
      url: ^/three$
      method: GET

   response:
      status: 200
      latency: 100
      body: 'Three!'

uuid (optional)

-  uuid: 9136d8b7-f7a7-478d-97a5-53292484aaf6
   request:
      method: GET
      url: /with/configured/uuid/property

   response:
      headers:
         content-type: application/json
      status: 200
      body: >
         {"status" : "OK"}

Back to top

Request

This object used to match an incoming request to stubby against the available endpoints that have been configured.

In YAML config, the request object supports the following properties:

url, method, query, headers, post, file

Keep on reading to understand their usage, intent and behavior.

Request object properties

Click to expand

url (required)

This is the simplest you can get:

-  request:
      url: /

A demonstration when not using regular expressions:

-  request:
      url: /some/resource/that/will/be/fully/matched

A demonstration using regular expressions:

-  request:
      url: ^/has/to/begin/with/this/

-  request:
      url: /has/to/end/with/this/$

-  request:
      url: ^/must/be/this/exactly/with/optional/trailing/slash/?$

-  request:
      url: ^/[a-z]{3}-[a-z]{3}/[0-9]{2}/[A-Z]{2}/[a-z0-9]+$

method (required)

-  request:
      url: /anything
      method: GET
-  request:
      url: /anything
      method: [GET, HEAD]

-  request:
      url: /anything
      method:
         -  GET
         -  HEAD

query (optional)

-  request:
      method: GET
      url: ^/with/parameters$
      query:
         type_name: user
         client_id: id
         client_secret: secret
         random_id: "^sequence/-/\\d/"
         session_id: "^user_\\d{32}_local"
         attributes: '["id","uuid","created","lastUpdated","displayName","email","givenName","familyName"]'

-  request:
      url: ^/with/parameters$
      query:
         search: search terms
         filter: month
-  request:
      url: ^/with/parameters$
      query:
         search:
         filter: month
-  request:
      url: ^/with/parameters$
      query:
         term: "boo and foo"
-  request:
      url: ^/with/parameters$
      query:
         term: "['stalin and truman']"

post (optional)

-  request:
      url: ^/post/form/data$
      post: name=John&email=john@example.com
-  request:
      method: [POST]
      url: /uri/with/post/regex
      post: "^[\\.,'a-zA-Z\\s+]*$"
-  request:
      url: ^/post/form/data$
      post: "^this/is/\\d/post/body"
-  request:
      method: POST
      url: /post-body-as-json
      headers:
         content-type: application/json
      post: >
         {"userId":"19","requestId":"(.*)","transactionDate":"(.*)","transactionTime":"(.*)"}

   response:
      headers:
         content-type: application/json
      status: 200
      body: >
         {"requestId": "<%post.1%>", "transactionDate": "<%post.2%>", "transactionTime": "<%post.3%>"}
-  request:
      method: POST
      url: /post-body-as-json-2
      headers:
         content-type: application/json
      post: >
         {"objects": [{"key": "value"}, {"key": "value"}, {"key": {"key": "(.*)"}}]}

   response:
      headers:
         content-type: application/json
      status: 200
      body: >
         {"internalKey": "<%post.1%>"}

file (optional)

-  request:
      method: POST
      headers:
         content-type: application/json
      file: ../json/post.payload.json
-  request:
      url: ^/match/against/file$
      file: postedData.json
      post: '{"fallback":"data"}'

postedData.json

{"fileContents":"match against this if the file is here"}

headers (optional)

The following endpoint only accepts requests with application/json post values:

-  request:
      url: /post/json
      method: post
      headers:
         content-type: application/json
         x-custom-header: "^this/is/\d/test"
         x-custom-header-2: "^[a-z]{4}_\\d{32}_(local|remote)"

Back to top

Request proxying

See request_proxying.html for details

Regex stubbing for dynamic matching

stubby supports regex stubbing for dynamic matching on the following properties:

Under the hood, stubby first attempts to compile the stubbed pattern into an instance of java.util.regex.Pattern class using the Pattern.MULTILINE flag. If the pattern compilation fails and PatternSyntaxException exception is thrown, stubby compiles the stubbed pattern into an instance of java.util.regex.Pattern class using the Pattern.LITERAL | Pattern.MULTILINE flags.

Please note, before using regex patterns in stubs, first it is best to ensure that the desired regex pattern “works” outside of stubby. One of the safest (and easiest) ways to test the desired pattern would be to check if the following condition is met: Pattern.compile("YOUR_PATTERN").matcher("YOUR_TEST_STRING").matches() == true.

The latter would ensure that the stubbed regex pattern actually works, also it is easier to debug a simple unit test case instead of trying to figure out why stub matching failed

Back to top

Regex stubbing for XML content

XML is not a regular language, it can be tricky to parse it using a regular expression (well, sometimes it is not as tricky when XML regex snippet is simple. But, most of the times this will cause you tears), especially when dealing with large XML POST payloads. XML is very complex: nested tags, XML comments, CDATA sections, preprocessor directives, namespaces, etc. make it very difficult to create a parse-able & working regular expression.

Click to expand

Therefore, stubby4j uses under the hood a full-fledged 3rd party XML parser - XMLUnit.

XMLUnit enables stubbing of XML content with regular expressions by leveraging XMLUnit-specific Regex match placeholders. Placeholders are used to specify exceptional requirements in the control XML document for use during equality comparison (i.e.: regex matching).

How to stub XML containing regular expressions?

  1. Using XMLUnit regular expression matcher placeholders [recommended]
  2. Using vanilla regular expressions

Using built-in XMLUnit regular expression matchers is a much more elegant, cleaner and less painful way to do XML-based regular expression matching. Also, you can still leverage the dynamic token replacement in stubbed response while using XMLUnit matchers!

Now, XMLUnit placeholder ${xmlunit.matchesRegex( ... )} to the rescue! Let’s understand how to use it, consider the following example of stubbed request and response objects:

- description: rule_1
  request:
    url: /some/resource/uri
    method: POST
    headers:
      content-type: application/xml
    post: >
      <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
      <idex:type xmlns:idex="http://idex.bbc.co.uk/v1">
          <idex:authority>${xmlunit.matchesRegex(.*)}</idex:authority>
          <idex:name>${xmlunit.matchesRegex(.*)}</idex:name>
          <idex:startsWith>${xmlunit.matchesRegex(.*)}</idex:startsWith>
      </idex:type>
  
  response:
      status: 200
      body: Captured values are, authority: <% post.1 %> with name <% post.2 %> that starts with <% post.3 %>

In the above example, the regular expressions defined in post XML will match any values inside idex:authority, idex:name and idex:startsWith elements, which then will be used to interpolate the template tokens in the stubbed response.

Please refer to the following XMLUnit Placeholders guide or/and their unit tests for more information.

Using vanilla regular expressions

Consider the following examples of stubbed request that have XML regex snippets under post:

- description: rule_1
  request:
    url: /some/resource/uri
    method: POST
    headers:
      content-type: application/xml
    post: >
      <\?xml version="1.0" encoding="UTF-8" standalone="yes"\?><idex:type xmlns:idex="http://idex.bbc.co.uk/v1"><idex:authority>(.*)</idex:authority><idex:name>(.*)</idex:name><idex:startsWith>(.*)</idex:startsWith></idex:type>

In the above example, do note that the ? in <?xml .. ?> are escaped (i.e.: <\?xml .. \?>) as these are regex specific characters.

- description: rule_1
  request:
    url: /some/resource/uri
    method: POST
    headers:
      content-type: application/xml
    post: >
      <\?xml version="1.0" encoding="UTF-8"\?>
      <person xmlns="http://www.your.example.com/xml/person">
          <VocabularyElement id="urn:epc:idpat:sgtin:(.*)">
              <attribute id="urn:epcglobal:product:drugName">(.*)</attribute>
              <attribute id="urn:epcglobal:product:manufacturer">(.*)</attribute>
              <attribute id="urn:epcglobal:product:dosageForm">(.*)</attribute>
              <attribute id="urn:epcglobal:product:strength">(.*)</attribute>
              <attribute id="urn:epcglobal:product:containerSize">(.*)</attribute>
          </VocabularyElement>
          <name>(.*)</name>
          <age>(.*)</age>
          <!--
            Hello,
               I am a multi-line XML comment
               <staticText>
                  <reportElement x="180" y="0" width="200" height="20"/>
                  <text><!\[CDATA\[(.*)\]\]></text>
                </staticText>
            -->
          <homecity xmlns="(.*)cities">
              <long>(.*)</long>
              <lat>(.*)</lat>
              <name>(.*)</name>
          </homecity>
          <one name="(.*)" id="urn:company:namespace:type:id:one">(.*)</one>
          <two id="urn:company:namespace:type:id:two" name="(.*)">(.*)</two>
          <three name="(.*)" id="urn:company:namespace:type:id:(.*)">(.*)</three>
      </person>

In the above example, do note that the:

  1. ? in <?xml .. ?> are escaped (i.e.: <\?xml .. \?>) as these are regex specific characters, and
  2. [ and ] in <![CDATA[ .. ]]> are escaped (i.e.: <!\[CDATA\[(.*)\]\]>) as these are regex specific characters

As you can see, using vanilla regular expressions in complex XML content is a much more brittle approach.

Back to top

Authorization Header

-  request:
      url: ^/path/to/basic$
      method: GET
      headers:
         # no "Basic" prefix nor explicit encoding in Base64 is required when stubbing,
         # just plain username:password format. Stubby internally encodes the value in Base64
         authorization-basic: "bob:password" 
   response:
      headers:
         Content-Type: application/json
      status: 200
      body: Your request with Basic was successfully authorized!

-  request:
      url: ^/path/to/bearer$
      method: GET
      headers:
         # no "Bearer" prefix is required when stubbing, only the auth value.
         # Stubby internally does not modify (encodes) the auth value
         authorization-bearer: "YNZmIzI2Ts0Q=="
   response:
      headers:
         Content-Type: application/json
      status: 200
      body: Your request with Bearer was successfully authorized!

-  request:
      url: ^/path/to/custom$
      method: GET
      headers:
         # custom prefix name is required when stubbing, followed by space & auth value.
         # Stubby internally does not modify (encodes) the auth value
         authorization-custom: "CustomAuthorizationType YNZmIzI2Ts0Q=="
   response:
      headers:
         Content-Type: application/json
      status: 200
      body: Your request with custom authorization type was successfully authorized!

Back to top

Response

Assuming a match between the incoming HTTP request and one of the stubbed request objects has been found, its stubbed response object properties used to build the HTTP response back to the client.

In YAML config, the response object supports the following properties:

status, body, file, headers, latency

Keep on reading to understand their usage, intent and behavior.

Response object properties

Click to expand
-  request:
      method: [GET,POST]
      url: /invoice/123

   response:
      status: 201
      headers:
         content-type: application/json
      body: OK


-  request:
      method: [GET]
      url: /uri/with/sequenced/responses

   response:
      -  status: 201
         headers:
            content-type: application/json
         body: OK

      -  status: 201
         headers:
            content-stype: application/json
         body: Still going strong!

      -  status: 500
         headers:
            content-type: application/json
         body: OMG!!!


-  request:
      method: [GET]
      url: /uri/with/sequenced/responses/infile

   response:
      -  status: 201
         headers:
            content-type: application/json
         file: ../json/sequenced.response.ok.json

      -  status: 201
         headers:
            content-stype: application/json
         file: ../json/sequenced.response.goingstrong.json

      -  status: 500
         headers:
            content-type: application/json
         file: ../json/sequenced.response.omfg.json


-  request:
      method: [GET]
      url: /uri/with/single/sequenced/response

   response:
      -  status: 201
         headers:
            content-stype: application/json
         body: Still going strong!

status (required)

-  request:
      url: ^/im/a/teapot$
      method: POST
   response:
      status: 420

body (optional)

-  request:
      url: ^/give/me/a/smile$
   response:
      body: ':)'
-  request:
      url: ^/give/me/a/smile$

   response:
      status: 200
      body: >
         {"status": "hello world with single quote"}
      headers:
         content-type: application/json
-  request:
      method: GET
      url: /atomfeed/1

   response:
      headers:
         content-type: application/xml
      status: 200
      body: <?xml version="1.0" encoding="UTF-8"?><payment><paymentDetail><invoiceTypeLookupCode/></paymentDetail></payment>
-  request:
      url: /1.1/direct_messages.json
      query:
         since_id: 240136858829479935
         count: 1
   response:
      headers:
         content-type: application/json
      body: https://api.twitter.com/1.1/direct_messages.json?since_id=240136858829479935&count=1

file (optional)

response:
      status: 200
      headers:
         content-type: application/json
      file: ../json/response.json
-  request:
      url: /
   response:
      file: extremelyLongJsonFile.json

headers (optional)

-  request:
      url: ^/give/me/some/json$
   response:
      headers:
         content-type: application/json
      body: >
         [{
            "name":"John",
            "email":"john@example.com"
         },{
            "name":"Jane",
            "email":"jane@example.com"
         }]

latency (optional)

-  request:
      url: ^/hello/to/jupiter$
   response:
      latency: 800000
      body: Hello, World!

Back to top

Dynamic token replacement in stubbed response

During HTTP request verification, you can leverage regex capturing groups (Regex stubbing for dynamic matching) as token values for dynamic token replacement in stubbed response.

Click to expand

stubby supports dynamic token replacement on the following properties:

Example

-  request:
      method: [GET]
      url: ^/regex-fileserver/([a-z]+).html$

   response:
      status: 200
      file: ../html/<% url.1 %>.html


-  request:
      method: [GET]
      url: ^/v\d/identity/authorize
      query:
         redirect_uri: "https://(.*)/app.*"

   response:
      headers:
         location: https://<% query.redirect_uri.1 %>/auth
      status: 302
  
            
-  request:
      method: [GET]
      url: ^/account/(\d{5})/category/([a-zA-Z]+)
      query:
         date: "([a-zA-Z]+)"
      headers:
         custom-header: "[0-9]+"

   response:
      status: 200
      body: Returned invoice number# <% url.1 %> in category '<% url.2 %>' on the date '<% query.date.1 %>', using header custom-header <% headers.custom-header.0 %>

Example explained

The url regex ^/account/(\d{5})/category/([a-zA-Z]+) has two defined capturing groups: (\d{5}) and ([a-zA-Z]+), query regex has one defined capturing group ([a-zA-Z]+). In other words, a manually defined capturing group has parenthesis around it.

Although, the headers regex does not have capturing groups defined explicitly (no regex sections within parenthesis), its matched value is still accessible in a template (keep on reading!).

Token structure

The tokens in response body follow the format of <% PROPERTY_NAME . CAPTURING_GROUP_ID %>. If it is a token that should correspond to headers or query regex match, then the token structure would be as follows: <% HEADERS_OR_QUERY . KEY_NAME . CAPTURING_GROUP_ID %>. Whitespace is allowed between the <% & %> and what’s inside.

Numbering the tokens based on capturing groups without sub-groups

When giving tokens their ID based on the count of manually defined capturing groups within regex, you should start from 1, not zero (zero reserved for token that holds full regex match) from left to right. So the leftmost capturing group would be 1 and the next one to the right of it would be 2, etc.

In other words <% url.1 %> and <% url.2 %> tokens correspond to two capturing groups from the url regex (\d{5}) and ([a-zA-Z]+), while <% query.date.1 %> token corresponds to one capturing group ([a-zA-Z]+) from the query date property regex.

Numbering the tokens based on capturing groups with sub-groups

In regex world, capturing groups can contain capturing sub-groups, as an example consider proposed url regex: ^/resource/ ( ([a-z]{3}) - ([0-9]{3}) ) $. In the latter example, the regex has three groups - a parent group ([a-z]{3}-[0-9]{3}) and two sub-groups within: ([a-z]{3}) & ([0-9]{3}).

When giving tokens their ID based on the count of capturing groups, you should start from 1, not zero (zero reserved for token that holds full regex match) from left to right. If a group has sub-group within, you count the sub-group(s) first (also from left to right) before counting the next one to the right of the parent group.

In other words tokens <% url.1 %>, <% url.2 %> and <% url.3 %> correspond to the three capturing groups from the url regex (starting from left to right): ([a-z]{3}-[0-9]{3}), ([a-z]{3}) and ([0-9]{3}).

Tokens with ID zero

Tokens with ID zero can obtain full match value from the regex they reference. In other words, tokens with ID zero do not care whether regex has capturing groups defined or not. For example, token <% url.0 %> will be replaced with the url full regex match from ^/account/(\d{5})/category/([a-zA-Z]+). So if you want to access the url full regex match, respectively you would use token <% url.0 %> in your template.

Another example, would be the earlier case where headers custom-header property regex does not have capturing groups defined within. Which is fine, since the <% headers.custom-header.0 %> token corresponds to the full regex match in the header custom-header property regex: [0-9]+.

It is also worth to mention, that the full regex match value replacing token <% query.date.0 %>, would be equal to the regex capturing group value replacing <% query.date.1 %>. This is due to how the query date property regex is defined - the one and only capturing group in the query date regex, is also the full regex itself.

Where to specify the template

You can specify template with tokens in both body as a string or using file by specifying template as external local file. When template is specified as file, the contents of local file from file will be replaced.

Alternatively, you can also template the path to the file itself:

-  request:
      method: [GET]
      url: ^/regex-fileserver/([a-z]+).html$

   response:
      status: 200
      file: ../html/<% url.1 %>.html

When the request is recieved and the regex matches, the path to the file will get resolved and the file content will be served if it exists.

-  request:
      method: POST
      url: /post-body-as-json
      headers:
         content-type: application/json
      post: >
         {"userId":"19","requestId":"(.*)","transactionDate":"(.*)","transactionTime":"(.*)"}

   response:
      headers:
         content-type: application/json
      status: 200
      body: >
         {"requestId": "<%post.1%>", "transactionDate": "<%post.2%>", "transactionTime": "<%post.3%>"}

Another example demonstrating the usage of tokens from the matched regex groups

When token interpolation happens

After successful HTTP request verification, if your body or contents of local file from file contain tokens - the tokens will be replaced just before rendering HTTP response.

Troubleshooting

Back to top

Stubbing HTTP 30x redirects

In order to stub a 30x HTTP redirect, you need to stub the following:

Example

- request:
    method: GET
    headers:
      content-type: application/json
    url: /item/redirect/source

  response:
    status: 301
    headers:
      location: /item/redirect/destination


- request:
    method: GET
    headers:
      content-type: application/json
    url: /item/redirect/destination

  response:
    headers:
      content-type: application/json
    status: 200
    body: >
      {"response" : "content"}

Example explained

Upon successful HTTP request verification, the /item/redirect/destination value of the stubbed response header location will be set as the value of the location header of the Jetty HTTP response, which will cause the redirect to another stub with url value /item/redirect/destination

Back to top

Record and replay

If body of the stubbed response contains a URL starting with http(s), stubby knows that it should record an HTTP response from the provided URL (before rendering the stubbed response) and replay the recorded HTTP response on each subsequent call.

Example

-  request:
      method: [GET]
      url: /maps/api/geocode/json
      query:
         address: "1600%20Amphitheatre%20Parkway,%20Mountain%20View,%20CA"
         sensor: false

   response:
      status: 200
      headers:
         content-type: application/json
      body: http://maps.googleapis.com

Example explained

Upon successful HTTP request verification, properties of stubbed request (method, url, headers, post and query) are used to construct an HTTP request to the destination URL specified in body of the stubbed response.

In the above example, stubby will record HTTP response received after submitting an HTTP GET request to the url below: http://maps.googleapis.com/maps/api/geocode/json?sensor=false&address=1600+Amphitheatre+Parkway,+Mountain+View,+CA

Please note

Back to top

Supplying stubbed endpoints to stubby

There are two ways available (listed in no particular order):

  1. Submit POST requests to localhost:8889 at runtime (check the admin_portal.html)
  2. Load a YAML config data-file (using -d / --data flags) with the following structure for each stubbed endpoint:

Back to top

Splitting main YAML config

There are situations where your main YAML config file will grow and become bloated due to large number of stubs, e.g.: your application talks to many downstream services.

stubby4j supports splitting the main YAML config file into multiple sub-config YAML files, which allows for more logical & cleaner stub code organisation (kudos fly to https://github.com/harrysun2006).

Example

Main data.yaml:

includes:
   - service-1-stubs.yaml
   - service-2-stubs.yaml
   - ...
   - ...
   - service-N-stubs.yaml

Example explained

You define the stubbed endpoints for each service (or any other logical organisation of stubs that suits your needs) in their own some-name-that-suits-you-N.yaml sub-config files.

When stubby parses the main data.yaml provided using -d / --data flags, all included sub-configs will be loaded as if all the stubs were defined in one YAML.

Please note

You cannot mix in the same YAML config the includes with sub-configs & defining stubs using request/response, e.g.: stubby4j will fail to load the following YAML:

includes:
   - service-1-stubs.yaml
   - service-2-stubs.yaml
   - service-3-stubs.yaml
     
-  request:
      method:
         -  GET
         -  POST
         -  PUT
      url: ^/resources/asn/.*$

   response:
      status: 200
      body: >
         {"status": "ASN found!"}
      headers:
         content-type: application/json

Back to top

Managing stubs configuration via the REST API

stubby4j enables you to manage your request / response definitions via the REST API exposed by the Admin Portal. See the available REST API summary

Back to Home