Optimizing URL Generation with APEX_UTIL.prepare_url: A Reliable URL Strategy for Oracle APEX

When working with Oracle APEX, the syntax of generating links via APEX_UTIL.prepare_url is dependent on the app session and whether Friendly URLs are enabled or not. Based on the session context, the function will generate absolute or relative URLs. These differences become apparent, for example, when prepare_url is called from outside your own APEX application, e.g. via a Scheduler Job. In this post, I’ll walk you through a robust PL/SQL solution that ensures reliable, absolute URLs regardless of the execution context — and explain why understanding this nuance is crucial for predictable link behavior.

When generating a link to an Oracle APEX page — such as for use in an email — it’s essential to ensure that the link includes the full, absolute web address. APEX offers two functions for this: APEX_PAGE.get_url and APEX_UTIL.prepare_url. In my example, I will consume APEX_UTIL.prepare_url to make it possible to rejoin sessions and create links with different types of checksums included. You will need to configure some options on application and page level (e.g. Page Access Protection and Session Management) to use checksums and to allow users to rejoin sessions.

Recently, I stumbled on a little challenge: sending emails including links to my APEX applications using different proxy domains. Since my app is deployed across multiple customers – each with their own APEX workspace and custom proxy domain – I needed to generate custom links. By using the proxy domains, customers can only access their APEX apps but not the ORDS Landingpage and APEX Workspace Login. On the other hand, the direct APEX instance URL provides access to all resources and can only be used by developers. The goal was to generate the link and consider both URLs depending on the purpose the link is created. Another problem was that the link generation should work in every context (inside and outside of an HTTP context). Since prepare_url does only return an absolute link that includes the server’s host address (FQDN) in case Friendly URLs are enabled in the APEX application and we are not within an HTTP context, it was necessary to modify the link and add the domain after APEX had generated it, in case it was missing.

To add a bit of flexibility and keep things dynamic, I went with the following approach:

  • To be able to generate a link with checksum, we need an active APEX session. If no session exists in the current execution context yet, we create one.
  • Take a customer-specific proxy domain saved in a database table; if it does not exist (e.g. for developers), take the Oracle APEX instance URL (APEX_MAIL.get_instance_url) as fallback.
  • If a relative URL is generated (starting with the SCRIPT_NAME path), we add the proxy or APEX instance URL to the link.

In my example, I have saved the proxy URLs in a table named “customer_proxies”. You need to include the SCRIPT_NAME path (“APEX_PATH”) for the proxy (e.g. https://customer1.example.com/ords/) as well as for the APEX instance (e.g. https://apex.example.com:8443/ords/) as well. With a proxy, the correct URL that needs to be saved depends on any rewrite rules you have defined on the proxy, so custom adjustments of the code below may be necessary. The Oracle APEX instance URL is a static value and needs to be set in the APEX internal workspace (instance admin) under Manage Instance > Instance Settings > Email > Oracle APEX Instance URL.

It’s important to note that the syntax of the resulting URL depends on the Friendly URLs setting of the APEX application in whose session context we are in. If you are in a session of an app with Friendly URLs activated, APEX will always generate a Friendly URL for you.

/** generates a fully qualified link to an APEX page with page items and checksum included
 *   @param pi_app_id specifies the app id
 *   @param pi_app_page_id specifies the app page id
 *   @param pi_app_user specifies the app user to be taken in case no APEX session exists yet
 *   @param pi_customer_id specifies a row in our customer proxy domain mapping table
 *   @param pi_checksum_type specifies the checksum type (NULL, PUBLIC_BOOKMARK, PRIVATE_BOOKMARK, SESSION)
 *   @param pi_page_item_names specifies the page item names (1...n)
 *   @param pi_page_item_values specifies the corresponding page item values (1...n)
 *   @return varchar2 - the generated link
**/
function generate_page_link
(
  pi_app_id           in number
, pi_app_page_id      in number
, pi_app_user         in varchar2 default sys_context('apex$session','app_user')
, pi_customer_id      in number default null
, pi_checksum_type    in varchar2 default null
, pi_page_item_names  in APEX_T_VARCHAR2 default null
, pi_page_item_values in APEX_T_VARCHAR2 default null
) return varchar2
as
  l_instance_url varchar2(500);
  l_proxy_url varchar2(500);
  l_script_name varchar2(100);
  l_session_id number;
  l_page_items varchar2(1000);
  l_prepared_url varchar2(4000);
  l_link varchar2(4000);
begin
  -- basic validation for input parameters
  if pi_app_id is null 
  or pi_app_page_id is null 
  or (    pi_page_item_names is not null and pi_page_item_values is not null
      and pi_page_item_names.count != pi_page_item_values.count)
  then
    raise_application_error(-20001, 'Some input parameters are missing or given page item names and values do not contain the same number of elements.');
  end if;

  -- if this function is not called within an active APEX app session (e.g. Scheduler Job), we need to create it to be able to generate the link with checksum
  if  pi_checksum_type is not null
  and APEX_CUSTOM_AUTH.get_session_id is null
  then
    APEX_SESSION.create_session(
      p_app_id   => pi_app_id,
      p_page_id  => pi_app_page_id,
      p_username => pi_app_user
    );
      
    l_session_id := APEX_CUSTOM_AUTH.get_session_id;
  end if;

  -- try to get a proxy domain that is used by clients to access the app
  -- in my example, each customer has their own proxy domain to access their own APEX application
  -- example: https://customer1.example.com/ords/
  begin
    select proxy_url
    into l_proxy_url
    from customer_proxies
    where customer_id = pi_customer_id;
  exception
    when no_data_found then
      l_proxy_url := null;
  end;
  
  -- get the direct Oracle APEX instance url
  -- the Oracle APEX instance url needs to be set in the internal workspace (instance admin)
  -- it is used if no proxy url was found
  -- example: https://apex.example.com:8443/ords/
  l_instance_url := APEX_MAIL.get_instance_url;

  -- create a comma-separated string of page_item_name:page_item_value pairs for the link
  if pi_page_item_names is not null and pi_page_item_values is not null
  then
    for i in 1 .. pi_page_item_names.count
    loop
      l_page_items := l_page_items || pi_page_item_names(i) || ':' || UTL_URL.escape(pi_page_item_values(i));

      if i < pi_page_item_names.count
      then
        l_page_items := l_page_items || ',';
      end if;
    end loop;
  end if;

  -- function results of APEX_UTIL.prepare_url are dependent on the app's friendly url configuration
  -- we need to consider two points in case friendly urls are used in the current app session:
  --  * if we are not in an HTTP context (there are no CGI variables), APEX_UTIL.prepare_url will return an absolute url including the APEX instance url (domain name)
  --  * in case we are in an HTTP context, the url generated with APEX_UTIL.prepare_url will start with the SCRIPT_NAME path ("APEX_PATH"), so we need to make sure to not have it twice
  -- technically, it is possible to control whether friendly urls are generated by manually setting APEX_APPLICATION.g_friendly_url to true or false before calling APEX_UTIL.prepare_url
  -- note: the function uses the static instance url (APEX_MAIL.get_instance_url) if it generates an absolute url and does not evaluate dynamic CGI environment variables such as HTTP_HOST in case we access ORDS via a proxy
  --
  -- examples: 
  --   g_friendly_url = true  - no HTTP context - https://apex.example.com:8443/ords/r/workspace_path_prefix/app_alias/page_alias?p10_item_value=xyz&clear=10&cs=ABCDEF123456
  --                          -    HTTP context - /ords/r/workspace_path_prefix/app_alias/page_alias?p10_item_value=xyz&clear=10&cs=ABCDEF123456
  --   g_friendly_url = false -                 - f?p=1234:10:::NO:10:P10_ITEM_VALUE:xyz&cs=ABCDEF123456
  l_prepared_url := APEX_UTIL.prepare_url(p_url => 'f?p=' || pi_app_id ||
                                                   ':' || pi_app_page_id ||
                                                   '::::' || pi_app_page_id ||
                                                   ':' || l_page_items
                                         ,p_checksum_type => pi_checksum_type
                                         ,p_plain_url => true);

  -- absolute url given
  if regexp_like(l_prepared_url, '^' || l_instance_url)
  then
    -- customer proxy url exists, so we can replace the instance url with the proxy url
    if l_proxy_url is not null
    then
      l_link := replace(l_prepared_url, l_instance_url, l_proxy_url);
    -- customer proxy url does not exist and the given url already includes the instance url, take it "as is"
    else
      l_link := l_prepared_url;
    end if;
  -- given url is relative
  -- the base url (proxy or instance url) needs to be added
  else
    -- get value of CGI variable SCRIPT_NAME (HTTP context)
    begin
      l_script_name := OWA_UTIL.get_cgi_env('SCRIPT_NAME');
    exception
      when others then
        l_script_name := null;
    end;
  
    -- SCRIPT_NAME path is included in given url
    if  l_script_name is not null
    and regexp_like(l_prepared_url, '^' || l_script_name)
    then
      if l_proxy_url is not null
      then
        -- if we have a proxy, remove trailing slash and SCRIPT_NAME path from given url to make sure we do not run into conflicts with proxy rewrite rules
        l_proxy_url := substr(l_proxy_url, 1, length(l_proxy_url) - 1);
        l_prepared_url := regexp_replace(l_prepared_url, '^' || l_script_name, '');
      else
        -- in case we are using the instance url, we need to remove the SCRIPT_NAME path
        l_instance_url := regexp_substr(l_instance_url, '^https?://([^/]+)', 1, 1, null, 0);
      end if;
    end if;

    -- priorize proxy url, take instance url as fallback
    l_link := coalesce(l_proxy_url, l_instance_url) || l_prepared_url;
  end if;

  -- clean up session if it was created before
  if  l_session_id is not null
  then
    APEX_SESSION.delete_session(p_session_id => l_session_id);
  end if;

  return l_link;
end generate_page_link;

We can generate the following URLs for example:

  • Proxy URL (Friendly URL):
    https://customer1.example.com/ords/r/sales1/forecast/dashboard?p1_order_id=20&clear=1&cs=abcd1234 …
  • Instance URL (Friendly URL):
    https://apex.example.com:8443/ords/r/sales1/forecast/dashboard?p1_order_id=20&clear=1&cs=abcd1234 …
  • Proxy URL (non-Friendly/legacy URL syntax):
    https://customer2.example.com/ords/f?p=1234:1:::NO:1:P1_ORDER_ID:20&cs=abcd1234 …
  • Instance URL (non-Friendly/legacy URL syntax):
    https://apex.example.com:8443/ords/f?p=1234:1:::NO:1:P1_ORDER_ID:20&cs=abcd1234 …

The key benefits of this approach are:

  • avoid issues caused by missing session context and handle session creation automatically if none exist
  • generation of absolute URLs for reliable use in emails and external systems – works with Friendly and non-Friendly URLs
  • take and priorize customer-specific proxy domains, use the direct Oracle APEX instance URL as fallback