OAuth Request Token Error - 500 Response from /oauth/request_token

41 views
Skip to first unread message

Shaurya Agrawal

unread,
Feb 4, 2026, 12:42:07 AMFeb 4
to fatsecret Platform API

Hello FatSecret Platform API Support Team,

I'm experiencing an issue with the OAuth 1.0a 3-legged flow when requesting a request token.

Issue:

When making a POST request to https://platform.fatsecret.com/oauth/request_token, I'm receiving a 500 error page (https://platform.fatsecret.com/error/500) instead of the expected OAuth token response.

Error Details:

Endpoint: POST https://platform.fatsecret.com/oauth/request_token

Response: HTML error page (500 Internal Server Error)

Expected: OAuth response with oauth_token and oauth_token_secret

What I've Verified:


✅ Using HTTPS (not HTTP)

✅ Consumer key and secret are correct

✅ OAuth signature is generated correctly (HMAC-SHA1, proper encoding)

✅ Callback URL is included in the request

Questions:

Is my callback URL registered correctly? I'm using: http://localhost:5001/venus-health-and-fitness/us-central1/api/fatsecret/oauth/callback for local development.

Where can I register/verify my callback URL in the developer portal?

Are there any specific requirements for the OAuth request that I might be missing?

Application Details:

Platform: Firebase Cloud Functions with Firestore

Application Name: Venus Health

Callback URL (Local): http://localhost:5001/venus-health-and-fitness/us-central1/api/fatsecret/oauth/callback

Callback URL (Production): https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback

Could you please help me resolve this issue? I'm happy to provide more details if needed.

Thank you,

Shaurya Bansal

shauryaag...@gmail.com

Venus Health & Fitness

seba...@fatsecret.com

unread,
Feb 4, 2026, 12:55:35 AMFeb 4
to fatsecret Platform API

Hi Shaurya,

Thanks for reaching out. A 500 Internal Server Error during the OAuth 1.0a "Request Token" phase is almost always a symptom of a Signature Base String mismatch or a strict encoding violation that the server cannot parse. While it feels like a wall, it is usually just a syntax hurdle.

Technical Troubleshooting for Venus Health

We have reviewed your draft, and while your technical points are solid, here is how we can refine the resolution for your specific Firebase environment:

  • The "Double-Encoding" Trap: For Firebase Cloud Functions, the oauth_callback URL often contains special characters (like colons and slashes). Ensure this URL is percent-encoded before it is used to generate the signature and encoded again when placed in the Authorization header.

  • The localhost vs. Production Constraint: While fatsecret does not require pre-registered whitelists, our security layer is highly sensitive to the consistency of the oauth_callback between the initial request and the final access token exchange.

  • OAuth 2.0—The Path of Least Resistance: Since you are building on a modern stack like Firebase and Firestore, I strongly recommend transitioning to our OAuth 2.0 implementation. It is significantly more robust for cloud-native applications and removes the headache of HMAC-SHA1 signature generation.

Shaurya Agrawal

unread,
Feb 4, 2026, 6:18:58 PMFeb 4
to fatsecret Platform API
Hello FatSecret Platform API Support Team,

Thank you for recommending OAuth 2.0 migration. We're planning to migrate from OAuth 1.0a to OAuth 2.0 for our Firebase/Firestore application.

We have a few questions to ensure a smooth migration:

1. Authorization Code Flow Support:
   - Does FatSecret OAuth 2.0 support authorization code flow (not just client credentials)?
   - Can we use OAuth 2.0 for user-specific endpoints like `foods.get_recently_eaten`?

2. OAuth 2.0 Endpoints:
   - What is the authorization endpoint URL for OAuth 2.0 authorization code flow?
   - What is the token exchange endpoint URL?
   - Are these different from the client credentials endpoints?

3. Scopes:
   - What scopes are required for user-specific data access (recently eaten, meal logging)?
   - Can we use the same Client ID/Secret for both client credentials and authorization code flows?

4. Redirect URIs:
   - How do we register redirect URIs for OAuth 2.0 authorization code flow?
   - Can we register multiple redirect URIs (localhost + production)?

5. Migration Path:
   - Is there a migration guide from OAuth 1.0a to OAuth 2.0?
   - Are there any breaking changes we should be aware of?

6. Token Management:
   - Do OAuth 2.0 access tokens expire? If so, what's the expiration time?
   - Is there a refresh token mechanism?
   - How do we handle token refresh?

Application Details:
- Platform: Firebase Cloud Functions with Firestore
- Application Name: Venus Health
- Current OAuth: OAuth 1.0a 3-legged flow (experiencing 500 errors)

Thank you for your assistance!

Best regards,
Shaurya
Backend Engineer
Venus Health & Fitness

seba...@fatsecret.com

unread,
Feb 4, 2026, 6:20:41 PMFeb 4
to fatsecret Platform API
Hi Shaurya,

Thanks for following up. It seems you are only using Profiles calls and all Profiles features are only available as part of OAuth1.0 hence scopes and associated implications are not available/required.

Happy to assist further

Shaurya Agrawal

unread,
Feb 5, 2026, 8:19:27 PM (13 days ago) Feb 5
to fatsecret Platform API
Subject: Request to Register OAuth Callback URL - 500 Error on Request Token Endpoint


Hello FatSecret Platform API Support Team,

Thank you for your previous assistance with the OAuth 1.0a implementation. We've implemented the double-encoding fix you recommended for Firebase Cloud Functions callback URLs, but we're still experiencing a 500 Internal Server Error when requesting a request token.

CURRENT ISSUE
=============

Error: 500 Internal Server Error from https://platform.fatsecret.com/oauth/request_token
Response: HTML error page (platform.fatsecret.com/error/500) instead of OAuth token response
Endpoint: POST https://platform.fatsecret.com/oauth/request_token

APPLICATION DETAILS
===================

- Application Name: Venus Health & Fitness
- Platform: Firebase Cloud Functions (Node.js)
- OAuth Flow: OAuth 1.0a 3-legged flow
- Consumer Key: 8aa81f1c0ec549f7978c44135cb40792

CALLBACK URL REGISTRATION REQUEST
==================================

We need to register the following OAuth callback URL in our FatSecret application settings:

https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback

Request: Could you please confirm if this callback URL is registered in our application settings, or guide us on how to register it through the developer portal?

IMPLEMENTATION DETAILS
======================

We've implemented the double-encoding fix you recommended:

1. Callback URL Encoding:
   - Percent-encode the callback URL BEFORE using it to generate the signature
   - Encode the callback URL AGAIN when placing it in the Authorization header

2. Code Implementation (functions/services/fatsecret/fatsecret-oauth.service.js):

   generateRequestTokenSignature(callbackUrl) {
       // Validate callback URL
       if (!callbackUrl || typeof callbackUrl !== 'string') {
           throw new Error('Invalid callback URL: must be a non-empty string');
       }

       // Validate URL format
       try {
           new URL(callbackUrl);
       } catch (error) {
           throw new Error(`Invalid callback URL format: ${error.message}`);
       }

       // Per FatSecret support: Percent-encode callback URL BEFORE using it to generate signature
       const encodedCallbackForSignature = this.encodeRFC3986(callbackUrl);

       const timestamp = Math.floor(Date.now() / 1000).toString();
       const nonce = crypto.randomBytes(16).toString('hex');

       // Use pre-encoded callback URL in params for signature generation
       const params = {
           oauth_consumer_key: consumerKey,
           oauth_nonce: nonce,
           oauth_signature_method: 'HMAC-SHA1',
           oauth_timestamp: timestamp,
           oauth_version: '1.0',
           oauth_callback: encodedCallbackForSignature  // Pre-encoded callback URL
       };

       // Sort parameters and build normalized params
       const sortedKeys = Object.keys(params).sort();
       const normalizedParams = sortedKeys
           .map(key => {
               const value = String(params[key]);
               // Encode both key and value per RFC 3986
               return `${this.encodeRFC3986(key)}=${this.encodeRFC3986(value)}`;
           })
           .join('&');

       // Build signature base string
       const encodedMethod = this.encodeRFC3986('POST');
       const encodedUrl = this.encodeRFC3986(this.requestTokenUrl);
       const encodedParams = this.encodeRFC3986(normalizedParams);
       const baseString = `${encodedMethod}&${encodedUrl}&${encodedParams}`;

       // Generate signature
       const signingKey = `${consumerSecret}&`;
       const signature = crypto
           .createHmac('sha1', signingKey)
           .update(baseString)
           .digest('base64');

       // Per FatSecret support: Encode callback URL AGAIN when placing in Authorization header
       const encodedCallbackForHeader = this.encodeRFC3986(encodedCallbackForSignature);

       // Build Authorization header
       const authParams = [
           `oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
           `oauth_nonce="${this.encodeRFC3986(nonce)}"`,
           `oauth_signature="${this.encodeRFC3986(signature)}"`,
           `oauth_signature_method="HMAC-SHA1"`,
           `oauth_timestamp="${timestamp}"`,
           `oauth_version="1.0"`,
           `oauth_callback="${encodedCallbackForHeader}"`  // Double-encoded for Authorization header
       ];

       return {
           Authorization: `OAuth ${authParams.join(', ')}`
       };
   }

3. RFC 3986 Encoding Function:

   encodeRFC3986(str) {
       return encodeURIComponent(str)
           .replace(/!/g, '%21')
           .replace(/'/g, '%27')
           .replace(/\(/g, '%28')
           .replace(/\)/g, '%29')
           .replace(/\*/g, '%2A');
   }

DEBUG LOGS
==========

Our implementation logs show the encoding is working correctly:

[FatSecret OAuth] Request token signature generation {
  callbackUrl: 'https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback',
  encodedCallbackForSignature: 'https%3A%2F%2Fus-central1-venus-health-and-fitness.cloudfunctions.net%2Fapi%2Ffatsecret%2Foauth%2Fcallback',
  encodedCallbackForHeader: 'https%253A%252F%252Fus-central1-venus-health-and-fitness.cloudfunctions.net%252Fapi%2Ffatsecret%2Foauth%2Fcallback',
  signatureBaseStringLength: 412,
  hasSignature: true,
  normalizedParams: 'oauth_callback=https%253A%252F%252Fus-central1-venus-health-and-fitness.cloudfunctions.net%252Fapi%2Ffatsecret%2Foauth%2Fcallback&oauth_consumer_key=8aa81f1c0ec549f7978c44135cb40792&oauth_nonce=...'
}

Observations:
- ✅ Callback URL is encoded before signature generation
- ✅ Callback URL is double-encoded for Authorization header
- ✅ Signature is generated successfully
- ✅ All OAuth parameters are included

REQUEST DETAILS
===============

Request URL: https://platform.fatsecret.com/oauth/request_token
Method: POST
Headers:
  Authorization: OAuth oauth_consumer_key="8aa81f1c0ec549f7978c44135cb40792", oauth_nonce="...", oauth_signature="...", oauth_signature_method="HMAC-SHA1", oauth_timestamp="...", oauth_version="1.0", oauth_callback="https%253A%252F%252Fus-central1-venus-health-and-fitness.cloudfunctions.net%252Fapi%2Ffatsecret%2Foauth%2Fcallback"
  Content-Type: application/x-www-form-urlencoded

Response: HTML error page (platform.fatsecret.com/error/500)

QUESTIONS
=========

1. Callback URL Registration: Is the callback URL "https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback" registered in our application settings? If not, how do we register it?

2. Callback URL Format: Does the callback URL need to match exactly (including protocol, domain, and path)? Are there any restrictions on callback URLs for Firebase Cloud Functions?

3. Encoding Verification: Based on our logs, is the double-encoding implementation correct? The callback URL appears to be properly encoded in both the signature base string and Authorization header.

4. Alternative Solutions: If callback URL registration is not the issue, what other causes could result in a 500 error with an HTML error page response?

ADDITIONAL CONTEXT
==================

We're using OAuth 1.0a 3-legged flow for accessing Profiles endpoints (specifically foods.get_recently_eaten). We understand that OAuth 2.0 is not available for Profiles endpoints, so we're committed to making OAuth 1.0a work correctly.

NEXT STEPS
==========

We're ready to:
- Register the callback URL if you can guide us on the process
- Provide additional logs or information if needed
- Test with different callback URL formats if required
- Make any necessary code adjustments based on your feedback

Thank you for your assistance. We look forward to resolving this issue and successfully implementing the OAuth flow.

Best regards,
Shaurya Agrawal

Backend Engineer
Venus Health & Fitness

---
Application Information:
- Consumer Key: 8aa81f1c0ec549f7978c44135cb40792
- Callback URL: https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback
- Platform: Firebase Cloud Functions (Node.js)
- OAuth Version: 1.0a (3-legged flow)

seba...@fatsecret.com

unread,
Feb 5, 2026, 8:30:59 PM (13 days ago) Feb 5
to fatsecret Platform API

Thank you for the detailed logs—this makes troubleshooting much faster.

You do not need to manually register that callback URL in the dashboard for OAuth 1.0a. The 500 Internal Server Error is occurring because of a Signature Mismatch caused by an encoding redundancy in your code.

The Issue: Double Encoding in the Base String Your debug logs reveal the problem clearly in the normalizedParams: oauth_callback=https%253A%252F%252F...

  • %3A is an encoded colon (:).

  • %253A is an encoded percent sign followed by 3A (%3A).

Your code encodes the callback once manually, and then your normalization loop (normalizedParams) encodes it a second time. This means you are signing a "double-encoded" string, while our server is verifying against a standard "single-encoded" string.

The Fix Please update your generateRequestTokenSignature function to pass the raw callback URL into the parameters object. Let your normalization logic handle the encoding (just like it handles the other parameters).

Here is the corrected logic:


generateRequestTokenSignature(callbackUrl) {
    // 1. Validate callback URL (Keep this)


    if (!callbackUrl || typeof callbackUrl !== 'string') {

        throw new Error('Invalid callback URL');


    }

    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = crypto.randomBytes(16).toString('hex');

    // 2. CHANGE: Use the RAW callbackUrl here. Do not pre-encode it.


    const params = {
        oauth_consumer_key: consumerKey,
        oauth_nonce: nonce,
        oauth_signature_method: 'HMAC-SHA1',
        oauth_timestamp: timestamp,
        oauth_version: '1.0',

        oauth_callback: callbackUrl // <--- PASS RAW URL
    };

    // 3. Normalization (Keep this logic - it will now encode the URL exactly once)


    const sortedKeys = Object.keys(params).sort();
    const normalizedParams = sortedKeys
        .map(key => {
            const value = String(params[key]);

            // This encodeRFC3986 call will handle the single necessary encoding for the callback


            return `${this.encodeRFC3986(key)}=${this.encodeRFC3986(value)}`;
        })
        .join('&');

    // 4. Base String Construction (Keep as is)


    const encodedMethod = this.encodeRFC3986('POST');
    const encodedUrl = this.encodeRFC3986(this.requestTokenUrl);
    const encodedParams = this.encodeRFC3986(normalizedParams);
    const baseString = `${encodedMethod}&${encodedUrl}&${encodedParams}`;

    // 5. Generate Signature (Keep as is)


    const signingKey = `${consumerSecret}&`;
    const signature = crypto
        .createHmac('sha1', signingKey)
        .update(baseString)
        .digest('base64');

    // 6. Header Construction
    // The header value should be the encoded URL.
    // Since we didn't pre-encode it, we encode it once here for the header.
    const finalCallbackForHeader = this.encodeRFC3986(callbackUrl);



    const authParams = [
        `oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
        `oauth_nonce="${this.encodeRFC3986(nonce)}"`,
        `oauth_signature="${this.encodeRFC3986(signature)}"`,
        `oauth_signature_method="HMAC-SHA1"`,
        `oauth_timestamp="${timestamp}"`,
        `oauth_version="1.0"`,

        `oauth_callback="${finalCallbackForHeader}"`


    ];

    return {
        Authorization: `OAuth ${authParams.join(', ')}`
    };
}


Next Steps

  1. Apply this change to remove the pre-encoding.

  2. Deploy and test the request token endpoint again.

If you receive a 200 OK after this, you are clear to proceed with the User Authorization step.

Shaurya Agrawal

unread,
Feb 8, 2026, 4:57:42 PM (10 days ago) Feb 8
to fatsecret Platform API
FatSecret Support Email - OAuth 1.0a Request Token Still Failing
Subject: OAuth 1.0a Request Token - 500 Error After Fixing Double Encoding

Hi FatSecret Support,

Thank you for identifying the double-encoding issue in my previous email. I've corrected the implementation as you suggested—the callback URL is now passed raw into the parameters object, and the normalization logic handles encoding exactly once.

However, I'm still receiving a 500 error when requesting the request token.

Corrected Implementation
Here is my updated generateRequestTokenSignature function:

generateRequestTokenSignature(callbackUrl) {
    const consumerKey = config.fatsecretConsumerKey.trim();
    const consumerSecret = config.fatsecretConsumerSecret.trim();


    const timestamp = Math.floor(Date.now() / 1000).toString();
    const nonce = crypto.randomBytes(16).toString('hex');

    // RAW callbackUrl - no pre-encoding

    const params = {
        oauth_consumer_key: consumerKey,
        oauth_nonce: nonce,
        oauth_signature_method: 'HMAC-SHA1',
        oauth_timestamp: timestamp,
        oauth_version: '1.0',
        oauth_callback: callbackUrl  // RAW URL
    };

    // Normalization - encodes exactly once

    const sortedKeys = Object.keys(params).sort();
    const normalizedParams = sortedKeys
        .map(key => {
            const value = String(params[key]);
            return `${this.encodeRFC3986(key)}=${this.encodeRFC3986(value)}`;
        })
        .join('&');

    // Base String Construction

    const encodedMethod = this.encodeRFC3986('POST');
    const encodedUrl = this.encodeRFC3986('https://platform.fatsecret.com/oauth/request_token');

    const encodedParams = this.encodeRFC3986(normalizedParams);
    const baseString = `${encodedMethod}&${encodedUrl}&${encodedParams}`;

    // Generate Signature

    const signingKey = `${consumerSecret}&`;
    const signature = crypto
        .createHmac('sha1', signingKey)
        .update(baseString)
        .digest('base64');

    // Authorization Header

    const finalCallbackForHeader = this.encodeRFC3986(callbackUrl);
    const authParams = [
        `oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
        `oauth_nonce="${this.encodeRFC3986(nonce)}"`,
        `oauth_signature="${this.encodeRFC3986(signature)}"`,
        `oauth_signature_method="HMAC-SHA1"`,
        `oauth_timestamp="${timestamp}"`,
        `oauth_version="1.0"`,
        `oauth_callback="${finalCallbackForHeader}"`
    ];

    return {
        Authorization: `OAuth ${authParams.join(', ')}`
    };
}
Debug Output (Single-Encoded ✅)
callbackUrl: 'https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback'

normalizedParams: 'oauth_callback=https%3A%2F%2Fus-central1-venus-health-and-fitness.cloudfunctions.net%2Fapi%2Ffatsecret%2Foauth%2Fcallback&oauth_consumer_key=8aa81f1c0ec549f7978c44135cb40792&oauth_nonce=9856bb01fa5ae5d64141d44a8907806b&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1770468656&oauth_version=1.0'

baseStringPreview: 'POST&https%3A%2F%2Fplatform.fatsecret.com%2Foauth%2Frequest_token&oauth_callback%3Dhttps%253A%252F%252Fus-central1-venus-health-and-fitness.cloudfunctions.net%252Fapi%252Ffatsecret%252Foauth%252Fcallback%26oauth_consumer_key%3D8aa81f1c0ec549f7978c44135cb40792%26oauth_nonce%3D...'
As you can see:

normalizedParams now shows https%3A%2F%2F (single-encoded ✅)
baseString correctly shows https%253A%252F%252F (double-encoded as per OAuth 1.0a spec for the base string ✅)
Still Receiving 500 Error
Despite the corrected encoding, FatSecret is still redirecting to /error/500.

Questions
Callback URL Registration
Do I need to register my callback URL in the FatSecret developer dashboard? If so, where exactly can I find this setting?

What is the exact format required for registering callback URLs? Should it include or exclude trailing slashes?

Can I use HTTPS callback URLs on a custom domain, or do I need to use a specific format?

Is there a whitelist of allowed callback URL patterns? My callback URL is a Google Cloud Functions endpoint.

OAuth Configuration
Is my consumer key enabled for OAuth 1.0a 3-legged flow? Some API keys are only enabled for 2-legged OAuth. Consumer key: 8aa81f1c0ec549f7978c44135cb40792

Do I need to explicitly enable the Premier API or 3-legged OAuth in my developer account settings?

Is there a separate OAuth 1.0a secret that differs from the REST API secret? I want to confirm I'm using the correct credentials.

Signature Generation
Is my base string format correct? I'm using: POST&<encoded_url>&<encoded_params>

Should the signing key be consumer_secret& (with trailing ampersand and no token secret) for the request token step?

Is the parameter order correct? I'm sorting alphabetically: oauth_callback, oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_version

Should oauth_callback be included in the Authorization header, or only in the signature base string?

Troubleshooting
Can you check your server logs for my recent requests to identify the exact cause of the 500 error? Timestamp: approximately 2026-02-07T12:50:58Z UTC

Is there a test/sandbox environment I can use to debug OAuth without affecting rate limits?

Can you provide a working example of a valid request token request with sample values so I can compare my implementation?

Account Status
Is my developer account in good standing and approved for OAuth 1.0a access?

Are there any pending approvals or verifications required before I can use the 3-legged OAuth flow?

Application Details
Consumer Key (partial): 8aa81f1c0ec549f7978c44135cb40792
Callback URL: https://us-central1-venus-health-and-fitness.cloudfunctions.net/api/fatsecret/oauth/callback
Request Token URL: https://platform.fatsecret.com/oauth/request_token
Signature Method: HMAC-SHA1
Thank you for your help!

Best regards, 
Shaurya

seba...@fatsecret.com

unread,
Feb 8, 2026, 5:02:09 PM (10 days ago) Feb 8
to fatsecret Platform API

Thank you for the detailed logs—this makes troubleshooting much faster.

You do not need to manually register that callback URL in the dashboard for OAuth 1.0a. The 500 Internal Server Error is occurring because of a Signature Mismatch caused by an encoding redundancy in your code.

The Issue: Double Encoding in the Base String Your debug logs reveal the problem clearly in the normalizedParams: oauth_callback=https%253A%252F%252F...

  • %3A is an encoded colon (:).

  • %253A is an encoded percent sign followed by 3A.

To Answer Your Specific Questions:

  1. Registration: No, you do not need to register the callback. If the signature is valid, we will accept the callback URL provided in the request.

  2. Format: Your format (https://...) is correct. There are no restrictions on Cloud Functions domains.

  3. Encoding: Your previous implementation was indeed double-encoding. The fix above resolves this.

Shaurya Agrawal

unread,
Feb 10, 2026, 8:07:28 PM (8 days ago) Feb 10
to fatsecret Platform API
FatSecret Support Email - Comprehensive Analysis & Request for Guidance
Subject: OAuth 1.0a Request Token - 500 Error Persisting After Multiple Fixes - Need Guidance

Hello FatSecret Platform API Support Team,

I'm writing to request your assistance with a persistent OAuth 1.0a request token issue that has persisted despite implementing all recommended fixes. We've been working through this issue for several days and have tried multiple approaches, but we're still receiving a 500 Internal Server Error.

Executive Summary
Issue: POST request to https://platform.fatsecret.com/oauth/request_token returns 500 error (HTML error page at platform.fatsecret.com/error/500)

Status: After fixing double-encoding issue and switching to oauth-1.0a library, error persists

Application: Venus Health & Fitness (Firebase Cloud Functions, Node.js)

Consumer Key: 8aa81f1c0ec549f7978c44135cb40792


Complete Timeline & Implementation History
Phase 1: Initial Issue (February 4, 2026)
Problem: Receiving 500 error on request token endpoint
Your Response: Identified double-encoding issue in callback URL
Action Taken: Fixed double-encoding by passing raw callback URL to normalization logic
Phase 2: After Double-Encoding Fix (February 6, 2026)
Status: Still receiving 500 error
Your Response: Confirmed encoding fix was correct, no callback URL registration needed
Action Taken: Verified single-encoding in normalizedParams and double-encoding in base string (per OAuth 1.0a spec)
Phase 3: Library-Based Implementation (February 10, 2026 - Current)
Change: Switched from manual signature generation to oauth-1.0a npm library
Rationale: Eliminate any potential manual encoding errors
Status: Still receiving 500 error
Current Implementation (Using oauth-1.0a Library)
We've now switched to using the oauth-1.0a npm library to eliminate any manual encoding errors. Here's our current implementation:

Code Implementation
generateRequestTokenSignature(callbackUrl) {
// Validate credentials
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured.');
}

const consumerKey = config.fatsecretConsumerKey.trim();
const consumerSecret = config.fatsecretConsumerSecret.trim();

// Validate callback URL
if (!callbackUrl || typeof callbackUrl !== 'string') {
throw new Error('Invalid callback URL: must be a non-empty string');
}

try {
new URL(callbackUrl);
} catch (error) {
throw new Error(`Invalid callback URL format: ${error.message}`);
}

// Use oauth-1.0a library for signature generation
// Library handles all encoding, parameter sorting, and signature generation
const requestData = {
method: 'POST',
data: {
oauth_callback: callbackUrl // Raw URL - library handles encoding
}
};

// Token is empty for request token phase
const token = {
key: '',
secret: ''
};

// Generate authorization data - library handles encoding automatically
const authData = this.oauth.authorize(requestData, token);
// Ensure oauth_callback is included in header
if (!authData.oauth_callback) {
authData.oauth_callback = callbackUrl;
}

// Convert to Authorization header format
const authHeader = this.oauth.toHeader(authData);

return authHeader;
}
Library Configuration
this.oauth = OAuth({
consumer: {
key: config.fatsecretConsumerKey,
secret: config.fatsecretConsumerSecret
},
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
}
});
Current Debug Output (Library-Based Implementation)
Latest Request Logs (February 10, 2026, ~13:31 UTC):

[FatSecret OAuth] Request token signature generation (using oauth-1.0a library) {
method: 'library-based',
hasAuthHeader: true,
authHeaderPreview: 'OAuth oauth_callback="https%3A%2F%2Fus-central1-venus-health-and-fitness.cloudfunctions.net%2Fapi%2Ffatsecret%2Foauth%2Fcallback", oauth_consumer_key="8aa81f1c0ec549f7978c44135cb40792", oauth_nonce="B...'
}

[FatSecret OAuth] Request token request {
hasAuthHeader: true,
authHeaderPreview: 'OAuth oauth_callback="https%3A%2F%2Fus-central1-venus-health-and-fitness.cloudfunctions.net%2Fapi%2Ffatsecret%2Foauth%2Fcallback", oauth_consumer_key="8aa81f1c0ec549f7978c44135cb40792", oauth_nonce="B...'
}
Response:

Status: 200 OK (HTTP status code is 200, but response body contains HTML error page)
Response URL: https://platform.fatsecret.com/error/500 (redirected or error page URL)
Content-Type: text/html
Body: HTML error page (<!DOCTYPE html>...Platform Error | fatsecret Platform API)
Note: The server returns HTTP 200 OK status, but the response body contains an HTML error page. This suggests either:

The server is returning an error page with a 200 status (unusual but possible)
There's a redirect happening that axios is following, resulting in a 200 status
The error page is being served at the /error/500 path with a 200 status
What We've Verified
✅ Implementation Correctness
Encoding: Using oauth-1.0a library which handles RFC 3986 encoding automatically
Callback URL: Single-encoded in Authorization header (https%3A%2F%2F...)
Signature Method: HMAC-SHA1
Parameter Order: Library handles alphabetical sorting
Base String: Library constructs according to OAuth 1.0a spec
Signing Key: consumer_secret& (empty token secret for request token phase)
✅ Configuration
Consumer Key/Secret: Valid 32-character hex strings
Callback URL: Valid HTTPS URL format
Endpoint: Correct (https://platform.fatsecret.com/oauth/request_token)
Method: POST
Headers: Only Authorization header (no Content-Type for empty body)
✅ Previous Fixes Applied
✅ Fixed double-encoding issue (per your February 6 guidance)
✅ Removed Content-Type header for empty body requests
✅ Switched to library-based implementation to eliminate manual errors
✅ Verified callback URL encoding matches OAuth 1.0a spec
Complete Request Details
Request:

Headers:
Authorization: OAuth oauth_callback="https%3A%2F%2Fus-central1-venus-health-and-fitness.cloudfunctions.net%2Fapi%2Ffatsecret%2Foauth%2Fcallback", oauth_consumer_key="8aa81f1c0ec549f7978c44135cb40792", oauth_nonce="<nonce>", oauth_signature="<signature>", oauth_signature_method="HMAC-SHA1", oauth_timestamp="<timestamp>", oauth_version="1.0"
Body: (empty)
Response:

HTTP Status Code: 200 OK
Content-Type: text/html
Response Body: HTML error page
- Contains: <!DOCTYPE html>...Platform Error | fatsecret Platform API
- Title: "Platform Error | fatsecret Platform API"
- URL in response: https://platform.fatsecret.com/error/500

Important Note:
The HTTP status code is 200 OK, but the response body contains an HTML error page
instead of the expected OAuth token response. Our code detects this by checking
for '<html', '<!DOCTYPE', or 'error' in the response body and treats it as an error.

This is unusual - typically a 500 error would return HTTP status 500, but FatSecret
appears to be returning a 200 status with an HTML error page in the body.
Questions & Request for Guidance
Given that we've:

Fixed the double-encoding issue you identified
Verified single-encoding in normalized params
Switched to a standard OAuth library to eliminate manual errors
Confirmed all parameters are present and correctly formatted
And we're still receiving a 500 error, we need your guidance on what to check next.

1. Consumer Key/Secret Verification
Question: Can you verify that consumer key 8aa81f1c0ec549f7978c44135cb40792 is:
Active and in good standing?
Enabled for OAuth 1.0a 3-legged flow (not just 2-legged)?
Associated with an account that has Premier API access (if required)?
Not expired or revoked?
2. Account/Application Status
Question: Is there any pending approval or verification required for our application to use OAuth 1.0a?
Question: Are there any account-level restrictions that might prevent OAuth 1.0a requests?
3. Server-Side Logs
Request: Can you check your server logs for requests from our consumer key around February 10, 2026, 13:31 UTC?
Request: What specific error is being logged on your side when you receive our request?
Request: Is the signature validation failing, or is there another issue?
4. Signature Verification
Question: Can you provide a sample valid request token request (with dummy values) so we can compare our implementation?
Question: Is there a test/sandbox endpoint we can use to debug without affecting rate limits?
Question: Are there any FatSecret-specific requirements beyond the standard OAuth 1.0a spec?
5. Library vs Manual Implementation
Question: Are you aware of any issues with the oauth-1.0a npm library when used with FatSecret's API?
Question: Should we revert to manual implementation, or is the library approach acceptable?
6. Callback URL Handling
Question: Despite your previous confirmation that callback URLs don't need registration, could there be any server-side validation of callback URL patterns?
Question: Are Google Cloud Functions domains (*.cloudfunctions.net) fully supported?
Question: Should the callback URL be included in the request body in addition to the Authorization header?
7. Alternative Approaches
Question: Given that OAuth 2.0 is not available for Profiles endpoints, are there any alternative authentication methods we should consider?
Question: Is there a different endpoint or flow we should be using for Firebase Cloud Functions?
What We Need Most
Primary Request: We need to understand what's causing the 500 error on your server side. The fact that we're receiving an HTML error page suggests the request is reaching your server, but something is failing during processing.

Specific Help Needed:

Server-side error details: What error is logged when you receive our request?
Signature validation: Is our signature being rejected? If so, what's the mismatch?
Account verification: Can you confirm our account/consumer key status?
Next steps: What should we try next?
Application Details
Application Name: Venus Health & Fitness
Platform: Firebase Cloud Functions (Node.js 20/24)
Consumer Key: 8aa81f1c0ec549f7978c44135cb40792
OAuth Flow: OAuth 1.0a 3-legged flow
Purpose: Access Profiles endpoints (foods.get_recently_eaten)
Library: oauth-1.0a npm package
Willingness to Collaborate
We're committed to making this work and are willing to:

Provide additional logs or debugging information
Test with different callback URLs or configurations
Make code changes based on your specific requirements
Schedule a call or screen share if helpful
Test in a sandbox environment if available
Conclusion
We've implemented all the fixes you've recommended and switched to a standard OAuth library to eliminate manual errors. The fact that we're still receiving a 500 error suggests there may be an account-level, configuration-level, or server-side issue that we cannot diagnose from our end.

We respectfully request your assistance in:

Checking server-side logs for our requests
Verifying our account/consumer key status
Providing guidance on what to check or try next
Thank you for your continued support. We look forward to resolving this issue and successfully implementing the OAuth 1.0a flow.

Best regards,

Shaurya Agrawal
Backend Engineer
Venus Health & Fitness

P.S. We understand that OAuth 1.0a is required for Profiles endpoints and that OAuth 2.0 is not available for this use case. We're fully committed to making OAuth 1.0a work correctly and appreciate your patience as we work through this issue.

seba...@fatsecret.com

unread,
Feb 10, 2026, 8:14:39 PM (8 days ago) Feb 10
to fatsecret Platform API
Thanks, looks like you still have an error in your signature generation, can you pls try:

async function requestOAuthToken(callbackUrl) {
    // Generate signature and authorization header
    const authHeader = this.generateRequestTokenSignature(callbackUrl);
   
    console.log('[FatSecret OAuth] Making request token request');
    console.log('[FatSecret OAuth] URL:', 'https://platform.fatsecret.com/oauth/request_token');
    console.log('[FatSecret OAuth] Auth Header:', authHeader);
   
    try {
        const response = await axios({
            method: 'POST',
            url: 'https://platform.fatsecret.com/oauth/request_token',
            data: null,  // or undefined, or just omit this line
            headers: authHeader,  // Only the Authorization header
            maxRedirects: 0,
            validateStatus: (status) => status < 500
        });
       
        console.log('[FatSecret OAuth] Response status:', response.status);
        console.log('[FatSecret OAuth] Response data:', response.data);
       
        return response.data;
       
    } catch (error) {
        console.error('[FatSecret OAuth] Request failed:', error.message);
        if (error.response) {
            console.error('[FatSecret OAuth] Response status:', error.response.status);
            console.error('[FatSecret OAuth] Response data:', error.response.data);
        }
        throw error;
    }
}
```

Shaurya Agrawal

unread,
Feb 11, 2026, 5:30:26 PM (7 days ago) Feb 11
to fatsecret Platform API
Please provide the whole updated working code, or mark where am i going wrong

Shaurya Agrawal

unread,
Feb 11, 2026, 5:30:41 PM (7 days ago) Feb 11
to fatsecret Platform API
it didnt work

FatSecret OAuth – test against emulator
=======================================
URL: http://localhost:5001/venus-health-and-fitness/us-central1/api/fatsecret/oauth/initiate
Auth: Bearer token

(Emulator console will show [FatSecret OAuth] request/response logs.)

Response status: 500
Response body: {
  "success": false,
  "error": "Failed to initiate OAuth flow. Please try again.",
  "code": "REQUEST_TOKEN_FAILED"
}

Request failed. Check emulator logs for [FatSecret OAuth] entries (signature, response status/data).



BELOW IS MY CODE

/**
* @fileoverview FatSecret OAuth Service
*
* Service for handling OAuth 1.0a 3-legged flow operations:
* - Request token generation
* - Access token exchange
* - Authorization URL generation
*
* @module services/fatsecret/fatsecret-oauth.service
* @requires oauth-1.0a
* @requires crypto
* @requires axios
* @requires @config/constant
* @since 1.0.0
*/

const OAuth = require('oauth-1.0a');
const crypto = require('crypto');
const axios = require('axios');
const config = require('@config/constant');

/**
* FatSecret OAuth Service Class
*/
class FatSecretOAuthService {
constructor() {
// Validate configuration
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
console.warn('[FatSecret OAuth] Warning: FatSecret credentials not configured. OAuth flow will fail.');
}

// Initialize OAuth 1.0a
this.oauth = OAuth({
consumer: {
key: config.fatsecretConsumerKey || '',
secret: config.fatsecretConsumerSecret || ''
},
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
}
});

// OAuth endpoints
this.requestTokenUrl = config.fatsecretOAuthRequestTokenUrl || 'https://platform.fatsecret.com/oauth/request_token';
this.authorizeUrl = config.fatsecretOAuthAuthorizeUrl || 'https://platform.fatsecret.com/oauth/authorize';
this.accessTokenUrl = config.fatsecretOAuthAccessTokenUrl || 'https://platform.fatsecret.com/oauth/access_token';
this.callbackUrl = config.fatsecretOAuthCallbackUrl;

// In-memory cache for request tokens (10-minute TTL)
this.requestTokenCache = new Map();
this.requestTokenTTL = 10 * 60 * 1000; // 10 minutes
}

/**
* Encode string per RFC 3986
* @param {string} str - String to encode
* @returns {string} Encoded string
*/
encodeRFC3986(str) {
return encodeURIComponent(str)
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
}

/**
* Generate OAuth signature for request token (manual implementation per FatSecret support).
* Uses raw callback URL and single encoding only to avoid signature mismatch from double encoding.
* Do not include oauth_token in request token phase.
*
* @param {string} callbackUrl - OAuth callback URL (raw, unencoded)
* @returns {Object} Authorization header
*/
generateRequestTokenSignature(callbackUrl) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}

const consumerKey = config.fatsecretConsumerKey.trim();
const consumerSecret = config.fatsecretConsumerSecret.trim();

if (!callbackUrl || typeof callbackUrl !== 'string') {
throw new Error('Invalid callback URL: must be a non-empty string');
}
try {
new URL(callbackUrl);
} catch (error) {
throw new Error(`Invalid callback URL format: ${error.message}`);
}

const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');

// Use RAW callbackUrl; normalization will encode it exactly once (per FatSecret)
const params = {
oauth_callback: callbackUrl,
oauth_consumer_key: consumerKey,
oauth_nonce: nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: timestamp,
oauth_version: '1.0'
};

const sortedKeys = Object.keys(params).sort();
// Normalized params: each name and value encoded once (RFC 5849 3.4.1.3.2 "encoded normalized request parameters string")
const normalizedParams = sortedKeys
.map(key => `${this.encodeRFC3986(key)}=${this.encodeRFC3986(String(params[key]))}`)
.join('&');

// Base string: method and URI are percent-encoded; third part is the normalized params string as-is (no second encoding)
const encodedMethod = this.encodeRFC3986('POST');
const encodedUrl = this.encodeRFC3986(this.requestTokenUrl);
const baseString = `${encodedMethod}&${encodedUrl}&${normalizedParams}`;

const signingKey = `${consumerSecret}&`;
const signature = crypto
.createHmac('sha1', signingKey)
.update(baseString)
.digest('base64');

const finalCallbackForHeader = this.encodeRFC3986(callbackUrl);
const authParams = [
`oauth_callback="${finalCallbackForHeader}"`,
`oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
`oauth_nonce="${this.encodeRFC3986(nonce)}"`,
`oauth_signature="${this.encodeRFC3986(signature)}"`,
`oauth_signature_method="HMAC-SHA1"`,
`oauth_timestamp="${timestamp}"`,
`oauth_version="1.0"`
];

const authHeader = {
Authorization: `OAuth ${authParams.join(', ')}`
};

if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Request token signature (manual, single-encode)', {
callbackUrl,
hasAuthHeader: true,
authHeaderPreview: authHeader.Authorization ? authHeader.Authorization.substring(0, 180) + '...' : 'none'
});
}
return authHeader;
}

/**
* Generate OAuth signature for access token exchange
* @param {string} requestToken - Request token
* @param {string} requestTokenSecret - Request token secret
* @param {string} verifier - OAuth verifier
* @returns {Object} Authorization header
*/
generateAccessTokenSignature(requestToken, requestTokenSecret, verifier) {
// Validate credentials exist before using
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}

const consumerKey = config.fatsecretConsumerKey.trim();
const consumerSecret = config.fatsecretConsumerSecret.trim();

const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');

const params = {
oauth_consumer_key: consumerKey,
oauth_token: requestToken,
oauth_nonce: nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: timestamp,
oauth_version: '1.0',
oauth_verifier: verifier
};

// Sort parameters
const sortedKeys = Object.keys(params).sort();
const normalizedParams = sortedKeys
.map(key => `${this.encodeRFC3986(key)}=${this.encodeRFC3986(String(params[key]))}`)
.join('&');

// Build signature base string
const encodedMethod = this.encodeRFC3986('POST');
const encodedUrl = this.encodeRFC3986(this.accessTokenUrl);
const encodedParams = this.encodeRFC3986(normalizedParams);
const baseString = `${encodedMethod}&${encodedUrl}&${encodedParams}`;

// Build signing key (consumer_secret&request_token_secret) - MUST use RAW secrets, NOT encoded
// According to OAuth 1.0a spec, signing key uses raw secret values
const signingKey = `${consumerSecret}&${requestTokenSecret}`;

// Generate signature
const signature = crypto
.createHmac('sha1', signingKey)
.update(baseString)
.digest('base64');

// Build Authorization header
const authParams = [
`oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
`oauth_token="${this.encodeRFC3986(requestToken)}"`,
`oauth_nonce="${this.encodeRFC3986(nonce)}"`,
`oauth_signature="${this.encodeRFC3986(signature)}"`,
`oauth_signature_method="HMAC-SHA1"`,
`oauth_timestamp="${timestamp}"`,
`oauth_version="1.0"`,
`oauth_verifier="${this.encodeRFC3986(verifier)}"`
];

return {
Authorization: `OAuth ${authParams.join(', ')}`
};
}

/**
* Parse OAuth response (query string format)
* @param {string} responseText - Response text
* @returns {Object} Parsed parameters
*/
parseOAuthResponse(responseText) {
const params = {};
const pairs = responseText.split('&');

for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
}

return params;
}

/**
* Initiate OAuth flow - get request token
* @param {string} userId - Firebase Auth user ID
* @returns {Promise<Object>} Request token and authorization URL
*/
async initiateOAuthFlow(userId) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}

if (!this.callbackUrl) {
throw new Error('OAuth callback URL not configured. Please set FATSECRET_OAUTH_CALLBACK_URL environment variable.');
}

try {
// Generate signature and authorization header (per FatSecret support guidance)
const authHeader = this.generateRequestTokenSignature(this.callbackUrl);

console.log('[FatSecret OAuth] Making request token request');
console.log('[FatSecret OAuth] URL:', this.requestTokenUrl);
console.log('[FatSecret OAuth] Auth Header:', authHeader);

// Request request token (per FatSecret support: data null, only Authorization header, maxRedirects 0)
const response = await axios({
method: 'POST',
url: this.requestTokenUrl,
data: null,
headers: authHeader,
maxRedirects: 0,
timeout: 30000,
validateStatus: (status) => status < 500
});

console.log('[FatSecret OAuth] Response status:', response.status);
console.log('[FatSecret OAuth] Response data:', response.data);

if (response.status >= 300 && response.status < 400) {
const location = response.headers?.location || '';
console.error('[FatSecret OAuth] Redirect received (signature or callback issue). Location:', location);
throw new Error(`FatSecret redirected (${response.status}) to: ${location}. Fix signature (single-encode callback only) or callback URL.`);
}

// Check if response is HTML (error page)
if (typeof response.data === 'string' && (response.data.includes('<html') || response.data.includes('<!DOCTYPE') || response.data.includes('error'))) {
// Log response for debugging
if (process.env.NODE_ENV !== 'production') {
console.error('[FatSecret OAuth] HTML error response received', {
status: response.status,
responsePreview: typeof response.data === 'string' ? response.data.substring(0, 500) : response.data
});
}
throw new Error('FatSecret returned an error page. This usually means: 1) Callback URL not registered in FatSecret app settings, 2) Invalid OAuth signature, or 3) Invalid credentials. Please check your FatSecret app configuration.');
}

// Check response status
if (response.status >= 400) {
throw new Error(`FatSecret API returned error status ${response.status}. Please check your callback URL is registered in FatSecret app settings and your credentials are valid.`);
}

// Parse response
const params = this.parseOAuthResponse(response.data);

if (!params.oauth_token || !params.oauth_token_secret) {
// Check if response contains error information
if (typeof response.data === 'string' && response.data.includes('error')) {
throw new Error(`FatSecret API error: ${response.data}. Please verify your callback URL is registered in FatSecret app settings.`);
}
throw new Error('Invalid response from FatSecret: missing oauth_token or oauth_token_secret. Please check your callback URL is registered in FatSecret app settings.');
}

if (params.oauth_callback_confirmed !== 'true') {
throw new Error('OAuth callback not confirmed by FatSecret');
}

const requestToken = params.oauth_token;
const requestTokenSecret = params.oauth_token_secret;

// Store request token in cache (10-minute TTL)
this.requestTokenCache.set(requestToken, {
requestTokenSecret,
userId,
expiresAt: Date.now() + this.requestTokenTTL
});

// Generate authorization URL
// Per FatSecret support: Ensure callback URL consistency between request token and access token exchange
// Use the same callback URL that was used in the request token signature
const authorizationUrl = `${this.authorizeUrl}?oauth_token=${this.encodeRFC3986(requestToken)}&oauth_callback=${this.encodeRFC3986(this.callbackUrl)}`;

if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Request token obtained', {
userId,
requestToken: requestToken.substring(0, 10) + '...',
expiresIn: this.requestTokenTTL / 1000
});
}

return {
requestToken,
requestTokenSecret,
authorizationUrl,
expiresIn: this.requestTokenTTL / 1000
};
} catch (error) {
console.error('[FatSecret OAuth] Request failed:', error.message);
console.error('[FatSecret OAuth] Failed to get request token', {
userId,
error: error.message,
response: error.response?.data,
status: error.response?.status,
statusText: error.response?.statusText
});
if (error.response) {
console.error('[FatSecret OAuth] Response status:', error.response.status);
console.error('[FatSecret OAuth] Response data:', error.response.data);
if (error.response.status >= 300 && error.response.status < 400 && error.response.headers?.location) {
console.error('[FatSecret OAuth] Redirect location:', error.response.headers.location);
}
const status = error.response.status;
const data = error.response.data;

// Check if response is HTML error page
if (typeof data === 'string' && (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('error/500'))) {
throw new Error('FatSecret returned an error page (500). Common causes:\n' +
'1. Callback URL not registered in FatSecret app settings\n' +
'2. Invalid OAuth signature (check consumer key/secret)\n' +
'3. Callback URL mismatch (must match exactly in FatSecret app)\n' +
'4. Invalid credentials\n\n' +
'Please verify your callback URL is registered in your FatSecret developer account at: https://platform.fatsecret.com/ (Sign in → Developers → Your Application → OAuth Settings)');
}

if (status === 401) {
throw new Error('FatSecret API authentication failed. Please check:\n' +
'1. FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET are correct\n' +
'2. Credentials are from FatSecret Platform API (not legacy API)\n' +
'3. Credentials are active and not expired');
} else if (status === 400) {
throw new Error('Invalid OAuth request (400). Please check:\n' +
'1. Callback URL is registered in FatSecret app settings\n' +
'2. Callback URL matches exactly (including http/https, port, path)\n' +
'3. OAuth signature is correct');
} else if (status === 500) {
throw new Error('FatSecret API server error (500). This usually means:\n' +
'1. Callback URL not registered in FatSecret app settings\n' +
'2. Invalid OAuth signature\n' +
'3. Server-side issue with FatSecret\n\n' +
'Please verify your callback URL in your FatSecret developer account at: https://platform.fatsecret.com/ (Sign in → Developers → Your Application → OAuth Settings)');
} else if (status >= 300 && status < 400) {
const location = error.response?.headers?.location || '';
throw new Error(`FatSecret redirected (${status}) to: ${location}. This often indicates an error page (e.g. /error/500). Check signature generation and callback URL.`);
} else if (status >= 400) {
throw new Error(`FatSecret API returned error ${status}. Response: ${typeof data === 'string' ? data.substring(0, 200) : JSON.stringify(data)}`);
}
}

// Network or other errors
if (error.code === 'ECONNABORTED') {
throw new Error('FatSecret API request timeout. Please try again.');
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to FatSecret API. Please check your internet connection.');
}

throw new Error(`Failed to get request token: ${error.message}`);
}
}

/**
* Exchange request token for access token
* @param {string} requestToken - Request token
* @param {string} verifier - OAuth verifier
* @param {string} userId - Firebase Auth user ID
* @returns {Promise<Object>} Access token and token secret
*/
async exchangeAccessToken(requestToken, verifier, userId) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured.');
}

// Retrieve request token secret from cache
const cached = this.requestTokenCache.get(requestToken);
if (!cached) {
throw new Error('Request token not found or expired. Please initiate OAuth flow again.');
}

if (cached.expiresAt < Date.now()) {
this.requestTokenCache.delete(requestToken);
throw new Error('Request token expired. Please initiate OAuth flow again.');
}

if (cached.userId !== userId) {
throw new Error('Request token does not match user. Please initiate OAuth flow again.');
}

const requestTokenSecret = cached.requestTokenSecret;

try {
// Generate OAuth signature
const authHeader = this.generateAccessTokenSignature(requestToken, requestTokenSecret, verifier);

// Exchange for access token
const response = await axios({
method: 'POST',
url: this.accessTokenUrl,
headers: {
...authHeader,
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 30000,
validateStatus: (status) => status < 500
});

// Check if response is HTML (error page)
if (typeof response.data === 'string' && (response.data.includes('<html') || response.data.includes('<!DOCTYPE') || response.data.includes('error'))) {
throw new Error('FatSecret returned an error page during token exchange. Please verify your request token and verifier are valid.');
}

// Check response status
if (response.status >= 400) {
throw new Error(`FatSecret API returned error status ${response.status} during token exchange.`);
}

// Parse response
const params = this.parseOAuthResponse(response.data);

if (!params.oauth_token || !params.oauth_token_secret) {
if (typeof response.data === 'string' && response.data.includes('error')) {
throw new Error(`FatSecret API error during token exchange: ${response.data}`);
}
throw new Error('Invalid response from FatSecret: missing oauth_token or oauth_token_secret');
}

// Clear request token from cache
this.requestTokenCache.delete(requestToken);

if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Access token obtained', {
userId,
accessToken: params.oauth_token.substring(0, 10) + '...'
});
}

return {
oauthToken: params.oauth_token,
oauthTokenSecret: params.oauth_token_secret
};
} catch (error) {
console.error('[FatSecret OAuth] Failed to exchange access token', {
userId,
error: error.message,
response: error.response?.data
});

// Clear request token from cache on error
this.requestTokenCache.delete(requestToken);

if (error.response) {
const status = error.response.status;
const data = error.response.data;

// Check if response is HTML error page
if (typeof data === 'string' && (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('error'))) {
throw new Error('FatSecret returned an error page during token exchange. Please verify:\n' +
'1. Request token is valid and not expired\n' +
'2. OAuth verifier is correct\n' +
'3. Request token matches the one from initiation');
}

if (status === 401) {
throw new Error('Invalid OAuth verifier or request token. Please initiate OAuth flow again.');
} else if (status === 400) {
throw new Error('Invalid OAuth request (400). Request token may be expired or invalid. Please initiate OAuth flow again.');
} else if (status >= 400) {
throw new Error(`FatSecret API returned error ${status} during token exchange.`);
}
}

throw new Error(`Failed to exchange access token: ${error.message}`);
}
}
}

// Export singleton instance
module.exports = new FatSecretOAuthService();



On Wednesday, 11 February 2026 at 06:44:39 UTC+5:30 seba...@fatsecret.com wrote:

Shaurya Agrawal

unread,
Feb 12, 2026, 5:44:47 PM (6 days ago) Feb 12
to fatsecret Platform API
hey can you please respond

seba...@fatsecret.com

unread,
Feb 12, 2026, 5:47:32 PM (6 days ago) Feb 12
to fatsecret Platform API

Looking at your code, we believe found the issue! There's a critical bug in your signature generation for the request token.

The Problem

In your generateRequestTokenSignature() method, you're missing the second encoding step for the normalized parameters in the base string:

javascript
// ❌ INCORRECT (current code) const baseString = `${encodedMethod}&${encodedUrl}&${normalizedParams}`;

This should be:

javascript
// ✅ CORRECT const baseString = `${encodedMethod}&${encodedUrl}&${this.encodeRFC3986(normalizedParams)}`;
Reply all
Reply to author
Forward
0 new messages