Defining a general and per operation rate limits on an API Product

I'm trying to set up rate limits in my API proxy in the following way:

  • By default, the rate limit for all operations is “20 requests every 1 minute”
  • For specific, more sensitive operations, the rate limit is “5 requests every 1 minute”

It seems that you can achieve this by setting a defining a general rate limit on an API Product, and then defining another rate limit on specific Operations under that API Product, as shown on the screenshot below:

image (1).png

But that doesn't work. When my Quota policy gets invoked, the apiproduct.developer.quota.* flow variables end up being set to the general rate limit. This is what I see in the debugger when making a request to /sensitive:

image (2).png

I think the problem is in how Operation precedence works in API Products, as documented here:

 More inclusive, less specific resource paths take predence over those that are more specific. For example, if you add / and /**, the / resource path takes precedence and the /** resource path is ignored.

I defined a catch-all / operation on the API Product, and it takes precedence over a more specific /sensitive operation.

I don’t want to explicitly define every possible resource path in my API Product, is there a different approach you’d recommend?

0 4 618
4 REPLIES 4

Hi @dpashkevich ,

That's a great use case, which makes me wonder why the operation match precedence works the way it does. I guess it originated as a convenience to not require all paths to be specified in an API Product. But then, when fine-grained operations with specific quota values was added it became an inconvenience.

Seems like a nice enhancement to the API Product feature, to invert the precedence

 

"moreSpecificMatchPrecedence":true

 

You could use an Access Entity policy as a workaround to get the API Product operation details, but it's not elegant.

For example, Access Entity for API Product

 

<AccessEntity name="AE-get-specific">
    <EntityType value="apiproduct"/>
    <EntityIdentifier ref="apiproduct.name" type="apiproductname"/>
    <OutputFormat>JSON</OutputFormat>
</AccessEntity>

 

Extract Variables for the specific Operation Config for the path, and also get the top level generic values as defaults:

 

<ExtractVariables name="EV-get-specific">
    <Source clearPayload="false">AccessEntity.AE-get-specific</Source>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <JSONPayload>
        <Variable name="limitArray" type="string">
            <JSONPath>$.operationGroup.operationConfigs[?(@.operations[0].resource == "{proxy.pathsuffix}")].quota.limit</JSONPath>
        </Variable>
        <Variable name="intervalArray" type="string">
            <JSONPath>$.operationGroup.operationConfigs[?(@.operations[0].resource == "{proxy.pathsuffix}")].quota.interval</JSONPath>
        </Variable>
        <Variable name="timeunitArray" type="string">
            <JSONPath>$.operationGroup.operationConfigs[?(@.operations[0].resource == "{proxy.pathsuffix}")].quota.timeUnit</JSONPath>
        </Variable>
        <Variable name="defaultLimit" type="string">
            <JSONPath>$.quota</JSONPath>
        </Variable>
        <Variable name="defaultInterval" type="string">
            <JSONPath>$.quotaInterval</JSONPath>
        </Variable>
        <Variable name="defaultTimeunit" type="string">
            <JSONPath>$.quotaTimeUnit</JSONPath>
        </Variable>
    </JSONPayload>
</ExtractVariables>

 

Create a Condition when there is a match in the API Product operation (e.g. ["/specific"]):

 

<Condition>limitArray != "[]"</Condition>

 

In this case cleanup the JSONPath results to get just the value by stripping first and last 2 characters and assigning to the specific values.

 

<AssignMessage name="AM-quota-variables">
    <AssignVariable>
        <Name>limit</Name>
        <Template>{substring(limitArray,2,-2)}</Template>
    </AssignVariable>
    <AssignVariable>
        <Name>interval</Name>
        <Template>{substring(intervalArray,2,-2)}</Template>
    </AssignVariable>
    <AssignVariable>
        <Name>timeunit</Name>
        <Template>{substring(timeunitArray,2,-2)}</Template>
    </AssignVariable>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

 

Create another Condition when no match in the API Product operation (e.g. /**):

 

<Condition>limitArray = "[]"</Condition>

 

In this case assign the default top level values.

 

<AssignMessage name="AM-quota-variables-default">
    <AssignVariable>
        <Name>limit</Name>
        <Template>{defaultLimit}</Template>
    </AssignVariable>
    <AssignVariable>
        <Name>interval</Name>
        <Template>{defaultInterval}</Template>
    </AssignVariable>
    <AssignVariable>
        <Name>timeunit</Name>
        <Template>{defaultTimeunit}</Template>
    </AssignVariable>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

 

Then finally, apply the Quota policy:

 

<Quota name="QU-impose-quota">
    <Distributed>true</Distributed>
    <Synchronous>true</Synchronous>
    <Identifier ref="client_id"/>
    <Allow count="420" countRef="limit"/>
    <Interval ref="interval">10</Interval>
    <TimeUnit ref="timeunit">day</TimeUnit>
</Quota>

 

The Proxy Preflow:

 

<PreFlow name="PreFlow">
        <Request>
            <Step>
                <Name>VK-query-param</Name>
            </Step>
            <Step>
                <Name>AE-get-specific</Name>
            </Step>
            <Step>
                <Name>EV-get-specific</Name>
            </Step>
            <Step>
                <Condition>limitArray != "[]"</Condition>
                <Name>AM-quota-variables</Name>
            </Step>
            <Step>
                <Condition>limitArray = "[]"</Condition>
                <Name>AM-quota-variables-default</Name>
            </Step>
            <Step>
                <Name>QU-impose-quota</Name>
            </Step>
        </Request>
        <Response/>
    </PreFlow>

 

And Bob's your uncle!

Thanks for taking the time to write a detailed response, Kurt! As you've called out, the workaround you propose is pretty complex, and potentially competes in complexity with just maintaining an explicit list of operations. I'll consider it.

A flag such as `moreSpecificMatchPrecedence` would be much appreciated, please consider it as a feature request. I was surprised by the current operation match precedence behavior myself.

I guess another alternative is to not store rate limit quotas on API Products directly, and instead store them e.g. in a KVM, and throw a couple of extra policies in your proxy to read the right config values from a KVM depending on requested resource, and then feed them into the Quota policy.

another alternative is to not store rate limit quotas on API Products directly, and instead store them e.g. in a KVM, and throw a couple of extra policies in your proxy to read the right config values from a KVM depending on requested resource, and then feed them into the Quota policy.

YES.

Yes, that's an option too.

I wanted to keep the API Product functionality as is. The only change is the implementation in the proxy to support the behavior you desire.