shopify_python_api: Incorrect URL generated when creating fulfillment records - API version 2023-01

Issue summary

When calling shopify.Fulfillment.create the resulting URL is incorrect preventing the successful creation of the Fulfillment record.

Expected behavior

The URL generated by the Shopify client library should be

/admin/api/<API_VERSION>/fulfillments.json

Actual behavior

The URL generated by the Shopify client library is

/admin/api/<API_VERSION>/orders//fulfillments.json

NOTE: The call to shopify.Fulfillment.create does not accept an order_id parameter.

Steps to reproduce the problem

  1. Create Shopify session with api_version set to ‘2023-01’
  2. Call shopify.Fulfillment.create with a valid fulfillment payload

Reduced test case

import logging
import shopify

logging.basicConfig(level=logging.DEBUG)

shop_uri = 'xxx.myshopify.com'
api_version = '2023-01'
access_token = 'shppa_xxx'

session = shopify.Session(shop_uri, api_version, access_token)
shopify.ShopifyResource.activate_session(session)

payload = dict(
    line_items_by_fulfillment_order=[dict(fulfillment_order_id="5894084460693", fulfillment_order_line_items=[
        dict(id="12190922277013", quantity="1")])])

shopify.Fulfillment.create(payload)

The best way to get your bug fixed is to provide a reduced test case.


Checklist

  • I have described this issue in a way that is actionable (if possible)

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 2
  • Comments: 20

Commits related to this issue

Most upvoted comments

So I have just stumbled upon this, and I found a temporary fix, by monkey-patching the prefix on the Fulfillment resource:

shopify.Fulfillment._prefix_source = ''

Added this on top of my script after import shopify and creating fulfillments for fullfillment_orders work.

Having said that, I know nothing about Python and very little about Shopify api so please do correct me if that’s some herecy. I used to be Ruby developer. Be kind to me please. 😉

Looking in the code I can see that the Order resource implements a function called _prefix that appears to allow for the URL to contain a path element “/customers” if a customer_id is included in the options, or omit the “/customers” path element if no customer_id is present.

class Order(ShopifyResource, mixins.Metafields, mixins.Events):
    _prefix_source = "/customers/$customer_id/"

    @classmethod
    def _prefix(cls, options={}):
        customer_id = options.get("customer_id")
        if customer_id:
            return "%s/customers/%s" % (cls.site, customer_id)
        else:
            return cls.site

would this approach work for the Fulfillment resource?

hello i’m trying the solution you say but it doesn’t work.

I am putting ("shopify.Fulfillment._prefix_source = “”) after calling the shopify library

and here I call the FulfillmentOrders function

fulfillment = shopify.FulfillmentOrders({ ‘order_id’: info.id, ‘line_items’: info.line_items, ‘location_id’: “63910969405” }) fulfillment.save()

when I run it I get this error

Traceback (most recent call last): File “/usr/local/lib/python3.11/site-packages/pyactiveresource/connection.py”, line 286, in _open http_response = self._handle_error(self._urlopen(request)) ^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/site-packages/pyactiveresource/connection.py”, line 316, in _urlopen return urllib.request.urlopen(request, timeout=self.timeout) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 216, in urlopen return opener.open(url, data, timeout) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 525, in open response = meth(req, response) ^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 634, in http_response response = self.parent.error( ^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 563, in error return self._call_chain(*args) ^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 496, in _call_chain result = func(*args) ^^^^^^^^^^^ File “/usr/local/lib/python3.11/urllib/request.py”, line 643, in http_error_default raise HTTPError(req.full_url, code, msg, hdrs, fp) urllib.error.HTTPError: HTTP Error 406: Not Acceptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File “/workspace/script_new.py”, line 385, in <module> fulfillment.save() File “/usr/local/lib/python3.11/site-packages/pyactiveresource/activeresource.py”, line 836, in save response = self.klass.connection.post( ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/site-packages/pyactiveresource/connection.py”, line 375, in post return self._open(‘POST’, path, headers=headers, data=data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/site-packages/shopify/base.py”, line 23, in _open self.response = super(ShopifyConnection, self)._open(args, *kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/site-packages/pyactiveresource/connection.py”, line 288, in _open http_response = self._handle_error(err) ^^^^^^^^^^^^^^^^^^^^^^^ File “/usr/local/lib/python3.11/site-packages/pyactiveresource/connection.py”, line 427, in _handle_error raise ClientError(err) pyactiveresource.connection.ClientError: Response(code=406, body=“b’'”, headers={‘Date’: ‘Mon, 10 Jul 2023 09:24:49 GMT’, ‘Content-Type’: ‘application/json’, ‘Transfer-Encoding’: ‘chunked’, ‘Connection’: ‘close’, ‘X-Sorting-Hat-PodId’: ‘330’, ‘X-Sorting-Hat-ShopId’: ‘58167164989’, ‘Referrer-Policy’: ‘origin-when-cross-origin’, ‘X-Frame-Options’: ‘DENY’, ‘X-ShopId’: ‘58167164989’, ‘X-ShardId’: ‘330’, ‘X-Stats-UserId’: ‘’, ‘X-Stats-ApiClientId’: ‘35414474753’, ‘X-Stats-ApiPermissionId’: ‘481241432395’, ‘X-Shopify-API-Terms’: 'By accessing or using the Shopify API you agree to the Shopify API License and Terms of Use at https://www.shopify.com/legal/api-terms’, ‘X-Shopify-API-Version’: ‘2023-01’, ‘HTTP_X_SHOPIFY_SHOP_API_CALL_LIMIT’: ‘1/40’, ‘X-Shopify-Shop-Api-Call-Limit’: ‘1/40’, ‘Strict-Transport-Security’: ‘max-age=7889238’, ‘Server-Timing’: ‘processing;dur=39’, ‘X-Shopify-Stage’: ‘production’, ‘Content-Security-Policy’: “default-src ‘self’ data: blob: ‘unsafe-inline’ ‘unsafe-eval’ https:// shopify-pos://; block-all-mixed-content; child-src ‘self’ https://* shopify-pos://; connect-src ‘self’ wss:// https://*; frame-ancestors ‘none’; img-src ‘self’ data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.shopifycs.com https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=043944de-41d2-41b0-98a4-e11a3a74230c”, ‘X-Content-Type-Options’: ‘nosniff’, ‘X-Download-Options’: ‘noopen’, ‘X-Permitted-Cross-Domain-Policies’: ‘none’, ‘X-XSS-Protection’: ‘1; mode=block; report=/xss-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=043944de-41d2-41b0-98a4-e11a3a74230c’, ‘X-Dc’: ‘gcp-europe-west3,gcp-europe-west3,gcp-europe-west3’, ‘X-Request-ID’: ‘043944de-41d2-41b0-98a4-e11a3a74230c’, ‘CF-Cache-Status’: ‘DYNAMIC’, ‘Set-Cookie’: ‘__cf_bm=IMXO3a5Jpv4z5ALoqtP1iEsJukdKlyRx448h2M8QXTw-1688981089-0-AZRQLsCQ7e7hkS3SZy3KxLTupFGbRO+RD01cnmcIzkJw4W5diUra0bo0h2mG5I2NIxb4CU8+WTXW2guyPSILKgs=; path=/; expires=Mon, 10-Jul-23 09:54:49 GMT; domain=.myshopify.com; HttpOnly; Secure; SameSite=None’, ‘Report-To’: ‘{“endpoints”:[{“url”:“https:\/\/a.nel.cloudflare.com\/report\/v3?s=Md4DqRc2f1F%2BUNTIyOrxWB41nAhol1E7ipixJtWzs%2Fh6hQ3nocRdkuKUYh5vFJ5hPB0VBeqBKeyrh2DxnnfxCTu9ngsLB%2B24%2B1SMOwWlQETrF%2BlHsunLiTj4NQRTrxtenPkkt8Zi80Ms”}],“group”:“cf-nel”,“max_age”:604800}’, ‘NEL’: ‘{“success_fraction”:0.01,“report_to”:“cf-nel”,“max_age”:604800}’, ‘Server’: ‘cloudflare’, ‘CF-RAY’: ‘7e47c181f8275c1a-FRA’, ‘alt-svc’: ‘h3=“:443”; ma=86400’}, msg=“Not Acceptable”)

what am I doing wrong

coolllllllllll,it works!!!

So I have just stumbled upon this, and I found a temporary fix, by monkey-patching the prefix on the Fulfillment resource:

shopify.Fulfillment._prefix_source = ''

Added this on top of my script after import shopify and creating fulfillments for fullfillment_orders work.

Having said that, I know nothing about Python and very little about Shopify api so please do correct me if that’s some herecy. I used to be Ruby developer. Be kind to me please. 😉

@hubertlepicki @bjquinn Great hack for that specific use case, but it breaks the ability to shopify.Fulfillment.find() a fulfillment because THAT request needs to go through the ‘…/order/<order_id>/fulfillments.json’ endpoint. I suppose you could wipe it at the beginning of your function and add it back again at the end.

def example:
   shopify.Fulfillment._prefix_source = ''
   ...
   shopify.Fulfillment._prefix_source = "/orders/$order_id/"