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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
- "./tests/system/test_apps/reporting_app:/opt/splunk/etc/apps/reporting_app"
- "./tests/system/test_apps/streaming_app:/opt/splunk/etc/apps/streaming_app"
- "./tests/system/test_apps/modularinput_app:/opt/splunk/etc/apps/modularinput_app"
- "./tests/system/test_apps/cre_app:/opt/splunk/etc/apps/cre_app"
- "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib"
- "./splunklib:/opt/splunk/etc/apps/reporting_app/bin/splunklib"
Expand Down
9 changes: 7 additions & 2 deletions splunklib/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ class Context:
:param headers: List of extra HTTP headers to send (optional).
:type headers: ``list`` of 2-tuples.
:param retries: Number of retries for each HTTP connection (optional, the default is 0).
NOTE: THIS MAY INCREASE THE NUMBER OF ROUNDTRIP CONNECTIONS
NOTE: THIS MAY INCREASE THE NUMBER OF ROUNDTRIP CONNECTIONS
TO THE SPLUNK SERVER AND BLOCK THE CURRENT THREAD WHILE RETRYING.
:type retries: ``int``
:param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s).
Expand Down Expand Up @@ -938,7 +938,12 @@ def request(
str(mask_sensitive_data(dict(all_headers))),
mask_sensitive_data(body),
)
if body:

if isinstance(body, str):
if method.upper() == "GET":
raise Exception("unable to set string body on GET method request")
message = {"method": method, "headers": all_headers, "body": body}
elif body:
body = _encode(**body)

if method == "GET":
Expand Down
50 changes: 50 additions & 0 deletions tests/system/test_apps/cre_app/bin/execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import splunk.rest
import json


class Handler(splunk.rest.BaseRestHandler):
def handle_GET(self):
self.response.setHeader("Content-Type", "application/json")
self.response.setHeader("x-foo", "bar")
self.response.status = 200
self.response.write(
json.dumps(
{
"headers": self.headers(),
"method": "GET",
}
)
)

def handle_DELETE(self):
self.handle_with_payload("DELETE")

def handle_POST(self):
self.handle_with_payload("POST")

def handle_PUT(self):
self.handle_with_payload("PUT")

def handle_PATCH(self):
self.handle_with_payload("PATCH")

def handle_with_payload(self, method):
self.response.setHeader("Content-Type", "application/json")
self.response.setHeader("x-foo", "bar")
self.response.status = 200
self.response.write(
json.dumps(
{
"payload": self.request.get("payload"),
"headers": self.headers(),
"method": method,
}
)
)

def headers(self):
return {
k: v
for k, v in self.request.get("headers", {}).items()
if k.lower().startswith("x")
}
20 changes: 20 additions & 0 deletions tests/system/test_apps/cre_app/default/app.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[id]
name = cre_app
version = 0.1.0

[package]
id = cre_app
check_for_updates = False

[install]
is_configured = 0
state = enabled

[ui]
is_visible = 1
label = [EXAMPLE] CRE app

[launcher]
description = Example app that exposes custom rest endpoints
version = 0.0.1
author = Splunk
5 changes: 5 additions & 0 deletions tests/system/test_apps/cre_app/default/restmap.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[script:execute]
match = /execute
scripttype = python
handler = execute.Handler

142 changes: 142 additions & 0 deletions tests/system/test_cre_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python
#
# Copyright © 2011-2025 Splunk, Inc.
#
# 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.

import json
import pytest

from tests import testlib
from splunklib import results


class TestJSONCustomRestEndpointsSpecialMethodHelpers(testlib.SDKTestCase):
app_name = "cre_app"

def test_GET(self):
resp = self.service.get(
app=self.app_name,
path_segment="execute",
headers=[("x-bar", "baz")],
)
self.assertIn(("x-foo", "bar"), resp.headers)
self.assertEqual(resp.status, 200)
self.assertEqual(
json.loads(str(resp.body)),
{
"headers": {"x-bar": "baz"},
"method": "GET",
},
)

def test_POST(self):
body = json.dumps({"foo": "bar"})
resp = self.service.post(
app=self.app_name,
path_segment="execute",
body=body,
headers=[("x-bar", "baz")],
)
self.assertIn(("x-foo", "bar"), resp.headers)
self.assertEqual(resp.status, 200)
self.assertEqual(
json.loads(str(resp.body)),
{
"payload": '{"foo": "bar"}',
"headers": {"x-bar": "baz"},
"method": "POST",
},
)

def test_DELETE(self):
# delete does allow specifying body and custom headers.
resp = self.service.delete(
app=self.app_name,
path_segment="execute",
)
self.assertIn(("x-foo", "bar"), resp.headers)
self.assertEqual(resp.status, 200)
self.assertEqual(
json.loads(str(resp.body)),
{
"payload": "",
"headers": {},
"method": "DELETE",
},
)


class TestJSONCustomRestEndpointGenericRequest(testlib.SDKTestCase):
app_name = "cre_app"

def test_no_str_body_GET(self):
def with_body():
self.service.request(
app=self.app_name, method="GET", path_segment="execute", body="str"
)

self.assertRaisesRegex(
Exception, "unable to set string body on GET method request", with_body
)

def test_GET(self):
resp = self.service.request(
app=self.app_name,
method="GET",
path_segment="execute",
headers=[("x-bar", "baz")],
)
self.assertIn(("x-foo", "bar"), resp.headers)
self.assertEqual(resp.status, 200)
self.assertEqual(
json.loads(str(resp.body)),
{
"headers": {"x-bar": "baz"},
"method": "GET",
},
)

def test_POST(self):
self.method("POST")

def test_PUT(self):
self.method("PUT")

def test_PATCH(self):
if self.service.splunk_version[0] < 10:
self.skipTest("PATCH custom REST endpoints not supported on splunk < 10")
self.method("PATCH")

def test_DELETE(self):
self.method("DELETE")

def method(self, method: str):
body = json.dumps({"foo": "bar"})
resp = self.service.request(
app=self.app_name,
method=method,
path_segment="execute",
body=body,
headers=[("x-bar", "baz")],
)
self.assertIn(("x-foo", "bar"), resp.headers)
self.assertEqual(resp.status, 200)
self.assertEqual(
json.loads(str(resp.body)),
{
"payload": '{"foo": "bar"}',
"headers": {"x-bar": "baz"},
"method": method,
},
)