API requests using GET method with body content

I have been perusing the OpenBoxes API documentation, and I have found several cases of an API that uses the HTTP GET method, but also necessitates the use of a JSON in the request body.

It has been my understanding that including request body content in a GET request is problematic because the semantics of such requests are left wholly undefined. That is, it is very unpredictable what various intervening proxies, caches, gateways and other tooling does with these kinds of requests; some forbid body content, some dispose of body content, some use only the URL to return cached content.

See this url for discussion of this topic:

The tooling I am using does not allow GET method to have body content without jumping through some hoops.

Do these APIs allow an alternate HTTP method (i.e. POST) to perform the same function?

Can you provide an example of where we use GET requests with a body?

GET requests are generally used to retrieve data (either a list of objects or a single object), POST requests are used to create objects and PUT requests are used to update existing objects.

We tend to use mostly GET for retrieve and POST for create and update (sometimes deletes). I’m pretty sure you can use PUT for updates and DELETE for deletes in most cases as well.

Blockquote

$ curl -i -X GET -H "Content-Type: application/json" -b cookies.txt \
-d '{ "offset":0, "max":1 }' https://openboxes.ngrok.io/openboxes/api/products


HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 10 Jun 2018 22:13:27 GMT
[{
    "id": "ff80818155df9de40155df9e329b0009",
    "productCode": "00003",
    "name": "Aspirin 20mg",
    "category": {
        "id": "1",
        "name": "Medicines"
    },
    "description": null,
    "dateCreated": "2016-07-12T14:58:55Z",
    "lastUpdated": "2016-07-12T14:58:55Z"
}]

Yeah if you want you can pass the data in the URL as request parameters.

curl -i -X GET -H "Content-Type: application/json" -b cookies.txt \ 
https://openboxes.ngrok.io/openboxes/api/products?max=1&offset=0

This is true in general wherever a GET is used with a request body? The json attributes can be passed as URL parameters?

I think this is generally true but let us know if you encounter an API where this doesn’t hold.

Just a note to inform you of a mistake in the documentation of the “Products” api. The document quoted above in this conversation implies that the payload of the response is a JSON array. This is in fact not true. The payload is in fact an object that contains two fields “data” which is an array, and “totalCount” which apparently gives a count of the number of elements available to be returned.

The /product/search endpoint is implemented using the GET method, and it seems to require json body content. It does not work to pass in the search arguments in URL query string. But, as stated before, body content on a get method is undefined in the HTTP RFC, so tooling does not allow for (i.e. Apache’s HTTPClient)

Thanks @Tim_McCollough. Can you provide an example of a search that isn’t working?

Using the example given in the Generic API documentation for searches. I have no idea how an array of search criteria would be converted to query parameters. Do you?

$ curl -i -X GET -H "Content-Type: application/json" -b cookies.txt \
-d '{"searchAttributes":[{"property":"name", "operator":"ilike", "value":"Ace%"}]}' \
https://openboxes.ngrok.io/openboxes/api/generic/product/search?max=1

Ah, the generic API. I’ll take a look and see if there’s something we can do here.

Disclaimer: The Generic APIs are there as a catch-all for our domain classes. They were meant to satisfy some basic requirements (search, retrieve, save) as we flesh out the APIs needed for the application.

You can also use the main Products API. There are two endpoints (list, search) that support basic search on products. See below for examples of how to use them.

The documentation for the APIs is admittedly pretty weak, so you’ll need to look at the code to get a better understanding of what is supported.

But, for what it’s worth, that’s why I created this discussion forum. Since we’re an open-source project with limited funding, we don’t have unlimited resources for writing documentation. Therefore, I deployed this Discourse instance because I wanted to make sure users and developers had the ability to get answers to their questions.

So don’t feel like you’re imposing by asking tons of questions. It’ll help us grow.

From my perspective, posting specific questions about what you are trying to implement does at least three things for the community:

  1. Allows us to help you (and others) to answer specific questions and move your projects forward
  2. Allows us to figure out what documentation we need to write
  3. Allows us to better understand what features we need to support

Here are a few examples. We could/should do a better job of standardizing these endpoints but again don’t hesitate to tell us what you’re looking to do and we’ll provide some ideas on how to achieve your goals.

Lookup by name, product code, etc

$ curl -b cookies.txt -X GET -H "Content-Type: application/json" 
https://demo.openboxes.com/openboxes/api/products?q=KS08
{
  "data": [
    {
      "id": "c1e344344072bbd501407a6d5fcd0003",
      "productCode": "KS08",
      "name": "Kojin?s",
      "description": "Juodos spalvos kojin?s",
      "category": "Apparel",
      "unitOfMeasure": "35",
      "pricePerUnit": 50,
      "dateCreated": "2013-08-14T01:25:23Z",
      "lastUpdated": "Sep 01, 2021",
      "updatedBy": "Anahy R",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null,
      "active": true
    }
  ],
  "totalCount": 1
}

Search by name, product code, etc

$ curl -b cookies.txt -X GET -H "Content-Type: application/json" 
https://demo.openboxes.com/openboxes/api/products/search?name=Ibuprofen
{
  "data": [
    {
      "id": "c1e344344072bbd50140ba2c40f4010a",
      "productCode": "BU45",
      "name": "Ibuprofen 200mg",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434413773c60141bcacee1000f1",
      "productCode": "PC26",
      "name": "Ibuprofen or Placebo",
      "color": null,
      "handlingIcons": [
        {
          "icon": "fa-snowflake",
          "color": "#3bafda",
          "label": "Cold chain"
        }
      ],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e344344200f2cb014253b54fcd009d",
      "productCode": "NK001",
      "name": "Ibuprofen, 200mg",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e3443443b6764c0143bfee85960019",
      "productCode": "JX55",
      "name": "Ibuprofen",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434445c2b5301448c50ba990038",
      "productCode": "KY96",
      "name": "Ibuprofen",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e344344fccfaf0014ffb173ed21214",
      "productCode": "GK86",
      "name": "Ibuprofen 200mg tablet",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434665700290166896b13fc3100",
      "productCode": "EE87",
      "name": "Ibuprofen, 200mg",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434665700290166b8a4464367f9",
      "productCode": "i001",
      "name": "Ibuprofen ",
      "color": null,
      "handlingIcons": [
        {
          "icon": "fa-exclamation-circle",
          "color": "#db1919",
          "label": "Controlled substance"
        }
      ],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e344346fecc3df01706fa5146801d9",
      "productCode": "ibuprofen 200",
      "name": "Ibuprofen 200mg tablet",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434727d92fb01749f93018a090c",
      "productCode": "ib002343153112",
      "name": "Ibuprofen 200mg",
      "color": null,
      "handlingIcons": [
        {
          "icon": "fa-snowflake",
          "color": "#3bafda",
          "label": "Cold chain"
        },
        {
          "icon": "fa-exclamation-triangle",
          "color": "#ffa500",
          "label": "Hazardous material"
        }
      ],
      "lotAndExpiryControl": null
    },
    {
      "id": "c1e34434766833b101773234427b02ad",
      "productCode": "GX24",
      "name": "Ibuprofen 200mg",
      "color": null,
      "handlingIcons": [
        {
          "icon": "fa-exclamation-circle",
          "color": "#db1919",
          "label": "Controlled substance"
        }
      ],
      "lotAndExpiryControl": null
    },
    {
      "id": "ff8081817f4160ad0180447b9ecf02de",
      "productCode": "7861194608327",
      "name": "Ibuprofen 200mg",
      "color": null,
      "handlingIcons": [
        {
          "icon": "fa-snowflake",
          "color": "#3bafda",
          "label": "Cold chain"
        }
      ],
      "lotAndExpiryControl": null
    },
    {
      "id": "ff8081818125768501829d16213503bf",
      "productCode": "0265748646",
      "name": "Ibuprofen 200mg",
      "color": null,
      "handlingIcons": [],
      "lotAndExpiryControl": true
    }
  ]
}

And you are correct, there’s no way for the search criteria to be converted to query parameters.

Also, can you describe what hoops you need to jump through to use a JSON body on GET requests using the Apache HttpClient?

The simplest solution I have found (using Stack Overflow) Is sub-classing the HttpPost class and then overriding the getMethod() to return “GET” rather than “POST”. I guess it isn’t that much of a hoop.

public static class HttpGetWithEntity extends HttpPost {

    public final static String METHOD_NAME = "GET";

    public HttpGetWithEntity(URI url) {
        super(url);
    }

    public HttpGetWithEntity(String url) {
        super(url);
    }

    @Override
    public String getMethod() {
        return METHOD_NAME;
    }
}

FYI: This does work and I am able to search using the generic product search.

Oh, nice. A little annoying but I’ve done similar things to work around similar issues.

In addition, I’ll look into what it would take to allow POST requests for advanced search APIs like this one.

I have been trying to create stock requests using /openboxes/api/stockMovements endpoint with the following JSON body but it does not work. I got error 500 - internal server error. Is there a way this can be fixed?

{
“errorCode”: 500,
“cause”: “java.lang.NullPointerException”,
“errorMessage”: “Cannot get property ‘id’ on null object”
}

JSON body
{
“name”: “my new stock movement2”,
“description”: “same as name”,
“identifier”: “483ZSA”,
“origin.id”: “2”,
“destination.id”: “1”,
“dateRequested”: “12/27/2023”,
“requestedBy.id”: “1”,
“lineItems”: [{
“product.id”: “ff8081818b2e8f2a018c9651ea8400f8”,
“quantityRequested”: “102”,
“sortOrder”: 0,
“recipient.id”: “1”
}]
}

Catalina.out

2023-12-28 07:10:51,323 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - stockMovementApi.create: [user:admin, location:null]
2023-12-28 07:10:51,323 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - No rule for stockMovementApi:create → allow anonymous
2023-12-28 07:10:51,326 [http-bio-0.0.0.0-8080-exec-366] INFO core.UserService - Is role ROLE_ADMIN in [ROLE_AUTHENTICATED, ROLE_SUPERUSER, ROLE_BROWSER, ROLE_MANAGER, ROLE_ASSISTANT, ROLE_ADMIN] = true
2023-12-28 07:10:51,350 [http-bio-0.0.0.0-8080-exec-366] ERROR errors.GrailsExceptionResolver - Exception occurred when processing request: [POST] /openboxes/api/stockMovements
Stacktrace follows:
java.lang.NullPointerException: Cannot get property ‘id’ on null object
at org.pih.warehouse.api.StockMovementApiController$_closure3.doCall(StockMovementApiController.groovy:113)
at org.pih.warehouse.api.StockMovementApiController$_closure3.call(StockMovementApiController.groovy)
at grails.plugin.springcache.web.GrailsFragmentCachingFilter.doFilter(GrailsFragmentCachingFilter.groovy:66)
at net.sf.ehcache.constructs.web.filter.Filter.doFilter(Filter.java:86)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:622)
at java.lang.Thread.run(Thread.java:748)
2023-12-28 07:10:51,353 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - errors.handleException: [user:admin, location:null]
2023-12-28 07:10:51,353 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - No rule for errors:handleException → allow anonymous
2023-12-28 07:10:51,354 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - Request duration for (errors/handleException): 1ms/0ms
2023-12-28 07:10:51,355 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - Request duration for (errors/handleException): 1ms/1ms

Sorry for the delay @Gideon. Most of us are on leave this week for Christmas. I can take a closer look at this next week, but I have a suggestion below.

My guess is that you haven’t logged into a location and some APIs don’t handle that gracefully yet. We plan to add some better validation and error handling to prevent this, but it looks like the docs are woefully quiet on the subject as well. So I’ve added a ticket to deal with that.

You can see what location the application thinks you’re logged into using the logging from the SecurityFilters mechanism.

2023-12-28 07:10:51,353 [http-bio-0.0.0.0-8080-exec-366] INFO filters.SecurityFilters - errors.handleException: [user:admin, location:null]

There are two ways to choose a location

  1. While authenticating, you can provide a location property in your JSON body

     curl -i -c cookies.txt -X POST -H "Content-Type: application/json" \
         -d '{"username": "<username>", "password": "<password>", "location": "<locationId>"}' \
         https://example.openboxes.com/openboxes/api/login
    
  2. After authenticating, you can call the Choose Location API.

     curl -i -c cookies.txt -X GET \
         https://example.openboxes.com/openboxes/api/chooseLocation/<locationId>
    

Hi Justin,
I understand the situation and I hope you are enjoying the holidays.

The issue was resolved when I added the location.
Thank you so much.