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: 18 additions & 0 deletions core/templating/template_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,24 @@ func (t templateHelpers) setStatusCode(statusCode string, options *raymond.Optio
return ""
}

func (t templateHelpers) setHeader(headerName string, headerValue string, options *raymond.Options) string {
if headerName == "" {
log.Error("header name cannot be empty")
return ""
}
internalVars := options.ValueFromAllCtx("InternalVars").(map[string]interface{})
var headers map[string][]string
if h, ok := internalVars["setHeaders"]; ok {
headers = h.(map[string][]string)
} else {
headers = make(map[string][]string)
}
// Replace or add the header
headers[headerName] = []string{headerValue}
internalVars["setHeaders"] = headers
return ""
}

func (t templateHelpers) sum(numbers []string, format string) string {
return sumNumbers(numbers, format)
}
Expand Down
16 changes: 15 additions & 1 deletion core/templating/template_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@ package templating
import (
"testing"
"time"

. "github.com/onsi/gomega"
)



// mockRaymondOptions is a minimal mock for raymond.Options for testing
type mockRaymondOptions struct {
internalVars map[string]interface{}
}

func (m *mockRaymondOptions) ValueFromAllCtx(key string) interface{} {
if key == "InternalVars" {
return m.internalVars
}
return nil
}


func testNow() time.Time {
parsedTime, _ := time.Parse("2006-01-02T15:04:05Z", "2018-01-01T00:00:00Z")
return parsedTime
Expand Down
16 changes: 13 additions & 3 deletions core/templating/templating.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func NewEnrichedTemplator(journal *journal.Journal) *Templator {
helperMethodMap["journal"] = t.parseJournalBasedOnIndex
helperMethodMap["hasJournalKey"] = t.hasJournalKey
helperMethodMap["setStatusCode"] = t.setStatusCode
helperMethodMap["setHeader"] = t.setHeader
helperMethodMap["sum"] = t.sum
helperMethodMap["add"] = t.add
helperMethodMap["subtract"] = t.subtract
Expand Down Expand Up @@ -146,11 +147,20 @@ func (t *Templator) RenderTemplate(tpl *raymond.Template, requestDetails *models

ctx := t.NewTemplatingData(requestDetails, literals, vars, state)
result, err := tpl.Exec(ctx)
if err == nil {
statusCode, ok := ctx.InternalVars["statusCode"]
if ok && response != nil {
if err == nil && response != nil {
// Set status code if present
if statusCode, ok := ctx.InternalVars["statusCode"]; ok {
response.Status = statusCode.(int)
}
// Set headers if present
if setHeaders, ok := ctx.InternalVars["setHeaders"]; ok {
if response.Headers == nil {
response.Headers = make(map[string][]string)
}
for k, v := range setHeaders.(map[string][]string) {
response.Headers[k] = v
}
}
}
return result, err
}
Expand Down
16 changes: 16 additions & 0 deletions core/templating/templating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,22 @@ func Test_ApplyTemplate_setStatusCode_should_handle_nil_response(t *testing.T) {
Expect(template).To(Equal(""))
}

func Test_ApplyTemplate_setHeader(t *testing.T) {
RegisterTestingT(t)

templator := templating.NewTemplator()

template, err := templator.ParseTemplate(`{{ setHeader "X-Test-Header" "HeaderValue" }}`)
Expect(err).To(BeNil())

response := &models.ResponseDetails{Headers: map[string][]string{}}
result, err := templator.RenderTemplate(template, &models.RequestDetails{}, response, &models.Literals{}, &models.Variables{}, make(map[string]string))

Expect(err).To(BeNil())
Expect(result).To(Equal(""))
Expect(response.Headers).To(HaveKeyWithValue("X-Test-Header", []string{"HeaderValue"}))
}

func toInterfaceSlice(arguments []string) []interface{} {
argumentsArray := make([]interface{}, len(arguments))

Expand Down
45 changes: 45 additions & 0 deletions docs/pages/keyconcepts/templating/templating.rst
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,51 @@ To learn about more advanced templating functionality, such as looping and condi

Global Literals and Variables
-----------------------------
Setting properties on the response
----------------------------------

Hoverfly provides helper functions to set properties on the HTTP response directly from your templates. This allows you to dynamically control the status code and headers based on request data or logic in your template.

Setting the Status Code
~~~~~~~~~~~~~~~~~~~~~~~
You can set the HTTP status code of the response using the ``setStatusCode`` helper. This is useful for conditional logic, such as returning a 404 if a resource is not found, or a 200 if an operation succeeds.

.. code:: handlebars

{{ setStatusCode 404 }}

You can use this helper inside conditional blocks:

.. code:: handlebars

{{#equal (csvDeleteRows 'pets' 'category' 'cats' true) '0'}}
{{ setStatusCode 404 }}
{"Message":"Error no cats found"}
{{else}}
{{ setStatusCode 200 }}
{"Message":"All cats deleted"}
{{/equal}}

If you provide an invalid status code (e.g., outside the range 100-599), it will be ignored.

Setting Response Headers
~~~~~~~~~~~~~~~~~~~~~~~
You can set or override HTTP response headers using the ``setHeader`` helper. This is useful for adding custom headers, controlling caching, or setting content types dynamically.

.. code:: handlebars

{{ setHeader "X-Custom-Header" "HeaderValue" }}

You can use this helper multiple times to set different headers, or inside conditional blocks to set headers based on logic:

.. code:: handlebars

{{ setHeader "Content-Type" "application/json" }}
{{ setHeader "X-Request-Id" (randomUuid) }}

If the header already exists, it will be overwritten with the new value.

Both helpers do not output anything to the template result; they only affect the response properties.
You can define global literals and variables for templated response. This comes in handy when you
have a lot of templated responses that share the same constant values or helper methods.

Expand Down
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python
import sys
import logging
import random
from time import sleep

logging.basicConfig(filename='random_delay_middleware.log', level=logging.DEBUG)
logging.debug('Random delay middleware is called')

# set delay to random value less than one second

SLEEP_SECS = random.random()

def main():

data = sys.stdin.readlines()
# this is a json string in one line so we are interested in that one line
payload = data[0]
logging.debug("sleeping for %s seconds" % SLEEP_SECS)
sleep(SLEEP_SECS)


# do not modifying payload, returning same one
print(payload)

if __name__ == "__main__":
main()
20 changes: 20 additions & 0 deletions functional-tests/hoverctl/sandbox-2112582993/testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDWDCCAkCgAwIBAgIVALg7EVVQrv7N34JFuuJg9LeW3R+QMA0GCSqGSIb3DQEB
CwUAMC8xGzAZBgNVBAoTEkhvdmVyZmx5IEF1dGhvcml0eTEQMA4GA1UEAxMHY2F0
LnBlbTAeFw0xNTExMjIxNzI4MjRaFw0xNzExMjExNzI4MjRaMC8xGzAZBgNVBAoT
EkhvdmVyZmx5IEF1dGhvcml0eTEQMA4GA1UEAxMHY2F0LnBlbTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAN+OtQ5fRqcieaPPKMhoi4MwEi2b8cakfeQI
3hqm+N5eJUIK5gC4bRydTUy4I4+GVxDuqG2+Ec+Ue7sYzWeXrArF1bCw/1KoNskE
HKUIcpu3BXfMzOZpYdcC5oKo3j+GimgkNOnZYmOdRzbPj+5jOSC9jWKH3Fb+Jn+Y
hL5ey67cIXZoXbqC0rJVOoKKRqbCR1z+YJ4Tn3z4jn5WktsdL2LiE62oxhAiQCr/
GkSuM3swvZ+5C55ftrCyq6vbTLwz39pQI1RM5QfQLQXiZgMW1K+sbdW0MUVGbjTp
uzP6QyRquajvRGP3uEhMyHLtVa/21y3XzDK/NL0IRwgS3pBEXPUCAwEAAaNrMGkw
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
MAMBAf8wHQYDVR0OBBYEFCK64Dm2Eadc3j1Moo9zqqQtLYzPMBIGA1UdEQQLMAmC
B2NhdC5wZW0wDQYJKoZIhvcNAQELBQADggEBAL1ft3tMwtbaOJvwi7SlJ1mtNgUt
fnvGkgUkzz44/ppe+Bq0LT0PaE2iLfvjaJbB6C/aYZPVooUjdzM86DUkGD/nNwzn
RrcUEKI40j2BtCVLbwvK0opAQgNlrAUptwz8jXbf04lpVuXCJ470+qQxMRDPvBZi
T80suZMmplH+mGnrGFOnW7tiq182BGZ9DCBynMfoJJdZfgQFD11trhk/2fs/ebIi
0/k95fP7E+c5PoWy9DbTcP7XpTf2yxHsPZujrUhDSaXYfyBOZ79yzj4W366tV9dj
md6kIskFx/FyADwu43z4TUaVpeHcK1IXrsrbWilgtSKsP8eLMmdSXvj2sqw=
-----END CERTIFICATE-----
13 changes: 13 additions & 0 deletions functional-tests/hoverctl/sandbox-2112582993/testdata/delays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"data": [
{
"urlPattern": "host1",
"delay": 100
},
{
"httpMethod": "POST",
"urlPattern": "host2",
"delay": 110
}
]
}
27 changes: 27 additions & 0 deletions functional-tests/hoverctl/sandbox-2112582993/testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA3461Dl9GpyJ5o88oyGiLgzASLZvxxqR95AjeGqb43l4lQgrm
ALhtHJ1NTLgjj4ZXEO6obb4Rz5R7uxjNZ5esCsXVsLD/Uqg2yQQcpQhym7cFd8zM
5mlh1wLmgqjeP4aKaCQ06dliY51HNs+P7mM5IL2NYofcVv4mf5iEvl7Lrtwhdmhd
uoLSslU6gopGpsJHXP5gnhOffPiOflaS2x0vYuITrajGECJAKv8aRK4zezC9n7kL
nl+2sLKrq9tMvDPf2lAjVEzlB9AtBeJmAxbUr6xt1bQxRUZuNOm7M/pDJGq5qO9E
Y/e4SEzIcu1Vr/bXLdfMMr80vQhHCBLekERc9QIDAQABAoIBAGUORGgHx49btTLI
PT5Ci/Y0b7MwUB2kU8gV/hh8K/mRAzSUap4ewCv0K3IntuN1LbxYtchN6A02qKvN
rWRLmpiQD2W8zN3XblR1yGENrYkYNZ+O83ygXHruM7cSkMyUi9JBs62V97Th7sQn
FYAEWFmlddj5Yy/r2QlKr47CmT4kKaw5DQsNjJnBLLvTkwHemT4tHDnIRrvKmR/I
lEh5OtI0AS8PxnpR54Mg2HOHlJteGPC5yw9FKetIoNkiQ8QpY278WV6tytRoRwA+
BFT7tELTXJjeOoMysB41RkbIqZXu1at0QRBug4m+m41f/FY5eMhduNrcerEH93yu
d/+0huECgYEA8euOKfEhi/dkL2GneTGamVlURr4uk4bjeaEI6mdTZ+//Jm8/Wdjr
y+VJ7D/5E5v9zaYxKzOoPTFScueIaLVeDsOuPGhTEPgCJZmyATCHGWIzrQXbBksx
ZKa1TDWpJbZ2N3NHJWyDLdVIZbCch+4P1Yj7bkxk70g4t1VGkvpwnmkCgYEA7JGP
eDdyyWKEPqGCgTp7z5rtoTdiUOAc+MbbDkx91hwB/2hHjXd+DtBdHpB9XoxL4d+i
i7JO8ETgueNT9o8Hu5F2Y8fI1g5gyBGBrZUqEpw5PE/luHDUcvsZh2MwF9nHOzxW
Tw72nov9R6+TmGDEQDen3Cu7K67xsK99ID9U0K0CgYAEuKMioGkWMTLMVeyNyfEJ
cxvY8Zc5G9XOptzkrjWLfryNBHjJCRm49fWWXb0/q7itTcQB4tUytIa2ZVxdJsT6
Jsl8tlCAsUZhc3ls2oSYczks9ENNASSqoTJClX2ClegCtwY5bb+1okbybRrw8C1w
7mZxxZ3mqZMpyMlCzw5pmQKBgCTovPqjpAwZi8p9xm/FISYN5P9fNb6qiLqjw++i
LYvnSMSBj5BZ0VgcWDr5jiXfO8Oc8a5b/obsKCe4eRQh0rIICvGcbRBApAby/EmJ
2UuYBjILwSqap/rchGokJo/CEZyDWG/zRLUN4FF76ko+5r+iL1VhmU1CeUD40Evk
taXtAoGAKuN8bdDe9GrTypcSZRb9YKvCu9yKODHPRhGYuqlGu6hlfUG9BVkmsccp
7ofzCXrJCf8RzUkEoKt0zlvzPUJizwJbWFfSLK9w0RR9bT2MxNz2aX+pR1OF3sk2
ZgyxyQ5GO1wKvgUOFI96zwdatMAKuYX4d+EBc/JystHNURByVBs=
-----END RSA PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":[{"requestTemplate": {"path": "/path1", "method": "GET", "destination": "www.virtual.com"}, "response": {"status": 201, "encodedBody": false, "body": "body1", "headers": {"Header": ["value1"]}}}, {"requestTemplate": {"path": "/path2", "method": "GET", "destination": "www.virtual.com", "headers": {"Header": ["value2"]}}, "response": {"status": 202, "body": "body2", "headers": {"Header": ["value2"]}}}]}
79 changes: 79 additions & 0 deletions functional-tests/hoverctl/sandbox-2112582993/testdata/sim1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"data": {
"pairs": [
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/"
}
],
"method": [
{
"matcher": "exact",
"value": "GET"
}
],
"destination": [
{
"matcher": "exact",
"value": "time.jsontest.com"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
],
"body": [
{
"matcher": "exact",
"value": ""
}
],
"query": {}
},
"response": {
"status": 200,
"body": "{\n \"date\": \"06-04-2019\",\n \"milliseconds_since_epoch\": 1559690466840,\n \"time\": \"11:21:06 PM\"\n}\n",
"encodedBody": false,
"headers": {
"Access-Control-Allow-Origin": [
"*"
],
"Content-Length": [
"100"
],
"Content-Type": [
"application/json"
],
"Date": [
"Tue, 04 Jun 2019 23:21:06 GMT"
],
"Hoverfly": [
"Was-Here"
],
"Server": [
"Google Frontend"
],
"X-Cloud-Trace-Context": [
"be9cf5bf37b02f6397e03f6477e302b0"
]
},
"templated": false
}
}
],
"globalActions": {
"delays": [],
"delaysLogNormal": []
}
},
"meta": {
"schemaVersion": "v5",
"hoverflyVersion": "v1.0.0",
"timeExported": "2019-06-05T00:21:35+01:00"
}
}
Loading
Loading