When you make an HTTPS REST call directly from Oracle Database, there are a couple of security layers that can get in the way before your request ever reaches the API.
In this guide, we'll take a practical approach and deliberately walk through those failures using Oracle AI Database 26ai Free running in Docker. We'll start with a REST call that fails, identify why it fails, fix the problem step by step, and finally get a successful HTTP 200 response.
Along the way, we'll cover:
- Why
UTL_HTTPcan fail withORA-24247 - How Oracle Network ACLs control outbound connections
- Why an HTTPS request can then fail with
ORA-29024 - What an Oracle Wallet actually does
- How to create an auto-login wallet using
orapki - How to import a trusted certificate into the wallet
- How to use the wallet with
UTL_HTTP - A common
APEX_WEB_SERVICEACL gotcha - Why
UTL_HTTP.SET_WALLETcan matter when database sessions are reused
![]() |
| Oracle Wallet + HTTPS REST Calls from a Dockerized Oracle Database |
Note: You should already have Oracle AI Database 26ai Free running in Docker and have SQLcl or another SQL client available. If you are completely new to running Oracle 26ai in Docker, check out my Oracle 26ai Docker setup video first. I'll add the video link in the Info section of this post.
What Are We Actually Trying to Do?
Our goal is simple: make an HTTPS REST request from inside Oracle Database.
For this demonstration, we'll have a small HTTPS endpoint running inside the same Oracle Database container:
Oracle Database 26ai → UTL_HTTP → HTTPS REST Endpoint
The endpoint will listen on:
https://localhost:8443/hello
The endpoint uses a self-signed certificate. This is intentional because it allows us to demonstrate the certificate trust problem clearly.
Our journey will look like this:
ORA-24247 → ORA-29024 → HTTP 200
Those three states are extremely useful because they show us that Oracle is dealing with two different security gates.
The Two Security Gates
Before an HTTPS REST request from Oracle succeeds, think about two separate questions:
- Gate 1 – Network ACL: Is this database principal allowed to connect to this host and port?
- Gate 2 – TLS Trust: Once Oracle reaches the endpoint, does Oracle trust the certificate presented by the server?
If the first gate fails, you get a network ACL error.
If the first gate succeeds but the certificate isn't trusted, you get a certificate validation error.
Only after both gates succeed do you finally get the HTTP response.
Step 1 – Prepare the HTTPS Test Endpoint
First, open a terminal and enter the running Oracle 26ai Docker container.
You can find your running container using:
docker ps
Then enter the container. Replace the container name if yours is different.
docker exec -it oracle26ai-apex261 bash
Create directories for our HTTPS demo server and wallet.
mkdir -p /opt/oracle/httpsdemo /opt/oracle/wallet
Create a Self-Signed Certificate
We'll create a self-signed certificate for localhost.
openssl req -x509 -nodes -newkey rsa:2048 -keyout /opt/oracle/httpsdemo/server.key -out /opt/oracle/httpsdemo/server.crt -days 365 -subj /CN=localhost -addext subjectAltName=DNS:localhost,IP:127.0.0.1
This produces two important files:
server.key– the private keyserver.crt– the server certificate
The certificate is deliberately self-signed. In a typical production environment, you would normally be dealing with a certificate issued by a trusted Certificate Authority (CA), or an internal enterprise CA.
Create the HTTPS Server
Create the Python HTTPS server:
cat > /opt/oracle/httpsdemo/demo_server.py <<'PYTHON'
import http.server
import ssl
import json
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
body = json.dumps({
"message": "Hello from HTTPS REST endpoint inside Oracle DB container!",
"path": self.path
}).encode()
```
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
```
httpd = http.server.HTTPServer(('0.0.0.0', 8443), Handler)
httpd.socket = ssl.wrap_socket(
httpd.socket,
certfile='/opt/oracle/httpsdemo/server.crt',
keyfile='/opt/oracle/httpsdemo/server.key',
server_side=True
)
httpd.serve_forever()
PYTHON
Start the HTTPS server in the background:
nohup python3 /opt/oracle/httpsdemo/demo_server.py > /opt/oracle/httpsdemo/server.log 2>&1 & disown
Give it a moment to start and test it with curl:
sleep 1 && curl -sk https://localhost:8443/hello; echo
You should receive something similar to:
{"message": "Hello from HTTPS REST endpoint inside Oracle DB container!", "path": "/hello"}
Notice that we're using -k with curl.
That tells curl to ignore certificate validation. We're doing this only to verify that our test HTTPS server is alive.
Oracle's UTL_HTTP does not simply ignore an untrusted certificate. That's exactly what we'll demonstrate next.
Step 2 – Make the REST Call Without a Network ACL
Now let's switch to our application schema.
sqlplus itov_schema@localhost:1521/FREEPDB1
We'll make a very simple HTTPS GET request using UTL_HTTP.
SET SERVEROUTPUT ON SIZE UNLIMITED
DECLARE
l_req UTL_HTTP.req;
l_resp UTL_HTTP.resp;
BEGIN
l_req := UTL_HTTP.begin_request(
'https://localhost:8443/hello',
'GET',
'HTTP/1.1'
);
l_resp := UTL_HTTP.get_response(l_req);
DBMS_OUTPUT.put_line('HTTP STATUS: ' || l_resp.status_code);
UTL_HTTP.end_response(l_resp);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line(
'ERROR STACK: ' || DBMS_UTILITY.format_error_stack
);
END;
/
The request fails.
You should see something similar to:
ERROR STACK: ORA-29273: HTTP request failed
ORA-24247: network access denied by access control list (ACL)
ORA-06512: at "SYS.UTL_HTTP", line 380
ORA-06512: at "SYS.UTL_HTTP", line 1189
This is our first important error:
ORA-24247: network access denied by access control list (ACL)
Oracle is stopping the request before we even get to the certificate validation stage.
Since Oracle 12c, outbound network access from database code is controlled through Network ACLs. The database principal needs permission to connect to the destination host and port.
Step 3 – Grant the Network ACL
Open another terminal and enter the Oracle container:
docker exec -it oracle26ai-apex261 bash
Connect as SYS:
sqlplus sys@localhost:1521/FREEPDB1 as sysdba
Now grant ITOV_SCHEMA permission to connect to localhost on port 8443.
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'localhost',
lower_port => 8443,
upper_port => 8443,
ace => xs$ace_type(
privilege_list => xs$name_list('connect'),
principal_name => 'ITOV_SCHEMA',
principal_type => xs_acl.ptype_db
)
);
END;
/
Now return to the first session and run the exact same UTL_HTTP request again.
This time, something interesting happens.
Step 4 – ORA-24247 Is Gone, But Now We Get ORA-29024
The error should now look like:
ERROR STACK: ORA-29273: HTTP request failed
ORA-29024: Certificate validation failure
ORA-06512: at "SYS.UTL_HTTP", line 380
ORA-06512: at "SYS.UTL_HTTP", line 1189
At first glance, this might look like another failure.
But actually, the changing error tells us that we've made progress.
The Network ACL gate has now been passed.
Oracle was allowed to connect to localhost:8443, reached the HTTPS server, and started certificate validation.
That's where the second gate failed.
ORA-29024 means that Oracle could not validate the server certificate against its trusted certificate information.
And this is where the Oracle Wallet comes in.
Step 5 – Create an Oracle Wallet
We'll create a wallet inside the Docker container.
Our wallet directory is:
/opt/oracle/wallet
Create the wallet using orapki:
orapki wallet create -wallet /opt/oracle/wallet -pwd 'WalletPwd123#_' -auto_login
The -auto_login option creates an auto-login wallet.
This gives us the cwallet.sso file, which allows the Oracle process to access the wallet without requiring us to put the wallet password directly into our PL/SQL call.
You can check the wallet directory:
ls -la /opt/oracle/wallet
Step 6 – Import the Trusted Certificate
Our HTTPS server is using the certificate we created earlier:
/opt/oracle/httpsdemo/server.crt
Import that certificate into the wallet as a trusted certificate:
orapki wallet add -wallet /opt/oracle/wallet -trusted_cert -cert /opt/oracle/httpsdemo/server.crt -pwd 'WalletPwd123#_'
Now display the wallet:
orapki wallet display -wallet /opt/oracle/wallet -pwd 'WalletPwd123#_'
Under the trusted certificates, you should see something similar to:
Trusted Certificates:
Subject: CN=localhost
We can also make sure the wallet is accessible by the Oracle operating-system user:
chmod 700 /opt/oracle/wallet
ls -la /opt/oracle/wallet
At this point, our wallet contains the trust information required for our test endpoint.
Important: In this demo we're trusting the self-signed server certificate directly. In a typical enterprise environment, you would generally import the appropriate issuing CA root and/or intermediate certificates rather than manually trusting individual server certificates.
Step 7 – Tell UTL_HTTP to Use the Wallet
Now that the wallet exists and contains the trusted certificate, we need to tell UTL_HTTP where it is.
Run the following in the ITOV_SCHEMA session:
UTL_HTTP.set_wallet(
'file:/opt/oracle/wallet',
''
);
Then make the HTTPS request again.
DECLARE
l_req UTL_HTTP.req;
l_resp UTL_HTTP.resp;
l_line VARCHAR2(32767);
BEGIN
UTL_HTTP.set_wallet(
'file:/opt/oracle/wallet',
''
);
l_req := UTL_HTTP.begin_request(
'https://localhost:8443/hello',
'GET',
'HTTP/1.1'
);
l_resp := UTL_HTTP.get_response(l_req);
DBMS_OUTPUT.put_line(
'HTTP STATUS: ' || l_resp.status_code
);
BEGIN
LOOP
UTL_HTTP.read_line(l_resp, l_line, TRUE);
DBMS_OUTPUT.put_line('BODY: ' || l_line);
END LOOP;
EXCEPTION
WHEN UTL_HTTP.end_of_body THEN
NULL;
END;
UTL_HTTP.end_response(l_resp);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line(
'ERROR STACK: ' || DBMS_UTILITY.format_error_stack
);
END;
/
And finally, we get our successful response:
HTTP STATUS: 200
BODY: {"message": "Hello from HTTPS REST endpoint inside Oracle DB container!", "path": "/hello"}
We have successfully crossed both security gates.
Network ACL permission is present, the certificate is trusted, and Oracle can now complete the HTTPS request.
The Complete Flow
Let's summarize what just happened:
- We attempted the REST call without a Network ACL.
- Oracle returned
ORA-24247. - We granted the Network ACL.
- The error changed to
ORA-29024. - That told us the network connection was now reaching the TLS layer.
- We created an Oracle Wallet.
- We imported the endpoint's trusted certificate.
- We configured
UTL_HTTPto use the wallet. - The same REST call returned
HTTP 200.
So the mental model is:
Network ACL → TLS Trust → HTTP Response
Bonus Gotcha #1 – APEX_WEB_SERVICE
Now let's take the same HTTPS endpoint and call it using APEX_WEB_SERVICE.
The basic call looks like this:
DECLARE
l_resp CLOB;
BEGIN
l_resp := APEX_WEB_SERVICE.make_rest_request(
p_url => 'https://localhost:8443/hello',
p_http_method => 'GET',
p_wallet_path => 'file:/opt/oracle/wallet',
p_wallet_pwd => ''
);
DBMS_OUTPUT.put_line(
'STATUS CODE: ' || APEX_WEB_SERVICE.g_status_code
);
DBMS_OUTPUT.put_line(
'BODY: ' || l_resp
);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line(
'ERROR STACK: ' || DBMS_UTILITY.format_error_stack
);
END;
/
But there is an important detail.
Even though we're executing the call from our application schema, the network ACL check can involve the execution context of the APEX package.
In this particular environment, the relevant package owner is APEX_260100.
So let's find the owner instead of assuming it.
Find the APEX Package Owner
Run:
SELECT DISTINCT owner
FROM all_objects
WHERE object_name = 'WWV_FLOW_WEB_SERVICES';
In this environment, the result is:
APEX_260100
This is a useful troubleshooting technique: when a package performs the network operation on your behalf, don't automatically assume that the schema issuing the call is the principal Oracle checks for the network ACL.
Grant the ACL to the APEX Owner
Connect with the required administrative privileges and grant the same host and port access to the APEX package owner:
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'localhost',
lower_port => 8443,
upper_port => 8443,
ace => xs$ace_type(
privilege_list => xs$name_list('connect'),
principal_name => 'APEX_260100',
principal_type => xs_acl.ptype_db
)
);
END;
/
Now run the APEX_WEB_SERVICE request again.
You should get:
STATUS CODE: 200
BODY: {"message": "Hello from HTTPS REST endpoint inside Oracle DB container!", "path": "/hello"}
This is one of those issues that can be confusing when everything appears to be configured correctly for your application schema.
If UTL_HTTP works but APEX_WEB_SERVICE returns ORA-24247, check the execution context and the relevant APEX package owner.
Bonus Gotcha #2 – SET_WALLET and Database Sessions
There is one more interesting behavior worth understanding.
Let's use another HTTPS endpoint: GitHub's API.
First, in a fresh ITOV_SCHEMA database session, grant access to GitHub:
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'api.github.com',
lower_port => 443,
upper_port => 443,
ace => xs$ace_type(
privilege_list => xs$name_list('connect'),
principal_name => 'ITOV_SCHEMA',
principal_type => xs_acl.ptype_db
)
);
END;
/
Now, without calling SET_WALLET first, make a request to GitHub:
DECLARE
l_req UTL_HTTP.req;
l_resp UTL_HTTP.resp;
l_line VARCHAR2(32767);
BEGIN
l_req := UTL_HTTP.begin_request(
'https://api.github.com/zen',
'GET',
'HTTP/1.1'
);
UTL_HTTP.set_header(
l_req,
'User-Agent',
'oracle-utl-http-demo'
);
l_resp := UTL_HTTP.get_response(l_req);
DBMS_OUTPUT.put_line(
'HTTP STATUS: ' || l_resp.status_code
);
UTL_HTTP.read_line(l_resp, l_line, TRUE);
DBMS_OUTPUT.put_line(
'BODY: ' || l_line
);
UTL_HTTP.end_response(l_resp);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line(
'ERROR STACK: ' || DBMS_UTILITY.format_error_stack
);
END;
/
In the tested environment, this returns:
HTTP STATUS: 200
BODY: Half measures are as bad as nothing at all.
So GitHub works.
Now, in the same database session, set our custom wallet:
BEGIN
UTL_HTTP.set_wallet(
'file:/opt/oracle/wallet',
''
);
END;
/
Retry the exact same GitHub request.
In our test environment, it now fails with:
ERROR STACK: ORA-29273: HTTP request failed
ORA-29024: Certificate validation failure
Why?
Our custom wallet contains the certificate trust material for our local self-signed endpoint. It does not contain the trust chain required by GitHub.
The important part is that we're still in the same database session.
In this tested environment, once SET_WALLET was applied, subsequent HTTPS certificate validation used that custom wallet.
Reconnecting with a fresh database session restored the previous behavior in the test.
This is particularly interesting when connection pooling is involved.
Why Session Reuse Matters
In a simple SQLcl or SQL*Plus session, you know exactly when your session starts and ends.
Production applications are often different.
Connection pools can keep database sessions alive and reuse them for different requests.
That means session-level state can become important.
For example, imagine this sequence:
- Request A uses
UTL_HTTP.SET_WALLETto call an internal HTTPS service. - The database session goes back into the connection pool.
- Request B receives the same session.
- Request B calls a completely different HTTPS endpoint.
- The trust material available to that session may now affect certificate validation.
The exact behavior depends on how your application, connection pool and Oracle environment are configured, so this should be tested in your own deployment.
But the practical lesson from this demonstration is simple:
Be conscious of wallet and session state when database sessions are reused.
If multiple HTTPS integrations are expected to use the same session, a wallet containing the appropriate trust anchors for those integrations can be preferable to switching wallets casually during application execution.
Common Errors and What They Mean
| Error | Likely Cause | What to Check |
|---|---|---|
ORA-24247 |
No Network ACL permission | Host, port and database principal |
ORA-24247 from APEX |
ACL is missing for the relevant APEX execution context | Package owner / APEX schema |
ORA-29024 |
Certificate validation failure | Wallet and trusted certificate chain |
HTTPS worked, then fails after SET_WALLET |
Session wallet state / incomplete trust store | Wallet contents and session lifecycle |
The Mental Model to Remember
Whenever you make an outbound HTTPS call directly from Oracle Database, think about these two gates:
1. NETWORK ACL
Can this database principal connect to this host and port?
2. TLS TRUST
Does Oracle trust the certificate presented by the server?
3. HTTP RESPONSE
Only after both gates succeed do you get your REST response.
That gives us a very useful troubleshooting sequence:
ORA-24247 → Fix ACL → ORA-29024 → Fix Wallet → HTTP 200
Final Takeaway
Calling an HTTPS REST API directly from Oracle Database isn't simply a matter of writing UTL_HTTP.begin_request().
Oracle needs to know two things:
- Whether the database principal is allowed to connect to the destination.
- Whether the HTTPS certificate presented by the destination can be trusted.
Network ACLs handle the first part, while the Oracle Wallet provides the trust material needed for the TLS layer.
Once you understand those two gates, errors such as ORA-24247 and ORA-29024 become much easier to troubleshoot.
And if you're working with Oracle APEX, remember that the schema executing the call and the schema involved in the network ACL check aren't necessarily the same. In our environment, APEX_WEB_SERVICE required the ACL to be granted to APEX_260100.
Finally, if you're using connection pools, keep session-level wallet behavior in mind. A wallet configured during one request can matter to subsequent HTTPS requests using that same database session.
That's the complete journey: from ORA-24247, through ORA-29024, all the way to HTTP 200.
Thanks for reading!
If you found this useful, check out Into the Oracle-verse for more practical Oracle Database, Oracle APEX, AI, automation and MCP tutorials.

No comments: