Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue: AWS Textract Service - 400 and CORS errors #6008

Open
1 of 8 tasks
WrathOP opened this issue Mar 1, 2025 · 2 comments
Open
1 of 8 tasks

Issue: AWS Textract Service - 400 and CORS errors #6008

WrathOP opened this issue Mar 1, 2025 · 2 comments
Labels
pending-maintainer-response Pending response from a maintainer of this repository pending-triage This issue is in the backlog of issues to triage

Comments

@WrathOP
Copy link

WrathOP commented Mar 1, 2025

Description

I'll help you craft a GitHub issue for the CORS and 404 errors you're experiencing. Here's a template you can use:

## Issue: AWS Textract Service - 404 in OPTIONS request and CORS errors

### Description
I'm experiencing CORS errors when trying to use the AWS Textract service from a Flutter application. The browser is sending an OPTIONS preflight request which is resulting in a 404 error, followed by CORS errors that prevent the actual Textract API call from succeeding.

### Environment
- Flutter version: Flutter 3.29.0
- AWS SDK packages:
  - aws_common: 
  - aws_signature_v4: 
- Browser : [Chrome.]
- Platform: [Web]

### Reproduction Steps
1. Initialize the AwsTextractService with valid AWS credentials
2. Call the `analyzeDocumentClarity` method with document bytes
3. The browser sends an OPTIONS preflight request to the Textract endpoint
4. The OPTIONS request receives a 404 response
5. Subsequent POST request fails with CORS error

### Code Example
```dart
final service = AwsTextractService.getInstance(
  accessKey: 'MY_ACCESS_KEY',
  secretKey: 'MY_SECRET_KEY',
  region: 'ap-south-1',
);

try {
  final result = await service.analyzeDocumentClarity(documentBytes);
  // Never gets here due to CORS error
} catch (e) {
  print('Error: $e');
}

Error Messages

Error analyzing document: POST https://textract.ap-south-1.amazonaws.com? failed: TypeError: Failed to fetch
DartError: Bad state: Future already completed

Expected Behavior

The AWS Textract API call should complete successfully without CORS errors.

Attempted Solutions

{"__type":"InvalidSignatureException","message":"The request signature we calculated does not match the signature you
provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
  • I've also tried to compare the client sdk for react and how it does things but couldn't replicate it.
  • I've verified the credentials
  • I've confirmed direct API calls (e.g., from Postman) work correctly.

Would appreciate guidance on:

  1. Is there a different approach needed for browser-based Textract calls?
  2. Are there additional headers needed for CORS support?

What I am trying to do.

import 'dart:convert';
import 'dart:typed_data';
import 'package:aws_textract_api/textract-2018-06-27.dart';
import 'package:aws_common/aws_common.dart';
import 'package:aws_signature_v4/aws_signature_v4.dart';
import 'package:crypto/crypto.dart';
import 'package:esc_pos_utils_plus/dart_hex/hex.dart';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';

/// AWS Textract service that uses the aws_signature_v4 package for authentication
class AwsTextractService {
  static AwsTextractService? _instance;

  final String accessKey;
  final String secretKey;
  final String region;
  final AWSCredentials _credentials;
  final AWSCredentialsProvider _credentialsProvider;
  late final AWSSigV4Signer _signer;
  final Uuid _uuid = const Uuid();

  // Private constructor for singleton
  AwsTextractService._({
    required this.accessKey,
    required this.secretKey,
    required this.region,
  })  : _credentials = AWSCredentials(accessKey, secretKey),
        _credentialsProvider =
            AWSCredentialsProvider(AWSCredentials(accessKey, secretKey)) {
    _signer = AWSSigV4Signer(
      credentialsProvider: _credentialsProvider,
    );
  }

  /// Get the singleton instance
  static AwsTextractService getInstance({
    required String accessKey,
    required String secretKey,
    String region = 'ap-south-1',
  }) {
    _instance ??= AwsTextractService._(
      accessKey: accessKey,
      secretKey: secretKey,
      region: region,
    );
    return _instance!;
  }

  String _formatDate(DateTime dateTime) {
    return '${dateTime.year}${dateTime.month.toString().padLeft(2, '0')}${dateTime.day.toString().padLeft(2, '0')}';
  }

  String _formatAmzDate(DateTime dateTime) {
    return '${_formatDate(dateTime)}T${dateTime.hour.toString().padLeft(2, '0')}${dateTime.minute.toString().padLeft(2, '0')}${dateTime.second.toString().padLeft(2, '0')}Z';
  }

  /// Analyze document clarity using AWS Textract
  /// Returns a confidence score between 0.0 and 1.0
  Future<double> analyzeDocumentClarity(Uint8List documentBytes) async {
    try {
      // Host and endpoint information
      final host = 'textract.$region.amazonaws.com';
      final endpoint = Uri.parse('https://$host');
      final timestamp = DateTime.now().toUtc();
      final amzDate = _formatAmzDate(timestamp);

      // Prepare the request body
      final requestBody = jsonEncode({
        'Document': {
          'Bytes': base64Encode(documentBytes),
        },
      });

      // Create a unique request ID
      final requestId = _uuid.v4();

      // Convert request body to bytes
      final requestBodyBytes = Uint8List.fromList(utf8.encode(requestBody));

      // Calculate SHA256 hash of request body
      final bodySha256 = sha256.convert(requestBodyBytes).bytes;
      final hexEncodedBodyHash = const HexEncoder().convert(bodySha256);

      // Prepare headers
      final headers = {
        'Content-Type': 'application/x-amz-json-1.1',
        'X-Amz-Target': 'Textract.DetectDocumentText',
        'amz-sdk-invocation-id': requestId,
        'amz-sdk-request': 'attempt=1; max=3',
        'Accept': '*/*',
        'X-Amz-Date': amzDate,
        'X-Amz-Content-Sha256': hexEncodedBodyHash,
      };

      debugPrint('Headers: $headers');

      // Create the AWS request
      final awsRequest = AWSHttpRequest(
        method: AWSHttpMethod.post,
        uri: endpoint,
        headers: headers,
        body: requestBodyBytes,
      );

      // Define the credential scope
      final scope = AWSCredentialScope(
        region: region,
        service: AWSService.textract,
      );

      debugPrint('Request: $awsRequest');

      // Sign the request
      final AWSSignedRequest signedRequest = await _signer.sign(
        awsRequest,
        credentialScope: scope,
        serviceConfiguration: S3ServiceConfiguration(), // Using S3 config as fallback
      );

      debugPrint('Signed Headers: ${signedRequest.headers}');

      // Send the request using AWS HTTP client
      final operation = signedRequest.send();

      // Handle the response
      final response = await operation.response;

      if (response.statusCode != 200) {
        final errorBody = await _decodeResponse(response);
        throw Exception(
          'AWS Textract request failed with status ${response.statusCode}: $errorBody',
        );
      }

      // Parse the response
      final responseBody = await _decodeResponse(response);
      final responseJson = jsonDecode(responseBody);
      final textractResponse = DetectDocumentTextResponse.fromJson(responseJson);

      // Calculate and return the document clarity score
      return _calculateDocumentClarityScore(textractResponse);
    } catch (e) {
      debugPrint('Error analyzing document: $e');
      rethrow;
    }
  }

  /// Helper method to decode the AWS HTTP response
  Future<String> _decodeResponse(AWSBaseHttpResponse response) async {
    return await utf8.decodeStream(response.split());
  }

  /// Calculate document clarity score based on Textract response
  double _calculateDocumentClarityScore(DetectDocumentTextResponse response) {
    final blocks = response.blocks;

    if (blocks == null || blocks.isEmpty) {
      return 0.0; // No text detected
    }

    // Calculate average confidence across all detected text blocks
    double totalConfidence = 0.0;
    int textBlockCount = 0;

    for (final block in blocks) {
      if (block.blockType == BlockType.line ||
          block.blockType == BlockType.word) {
        if (block.confidence != null) {
          totalConfidence += block.confidence!;
          textBlockCount++;
        }
      }
    }

    // If no text blocks found, document might be blank or an image
    if (textBlockCount == 0) {
      return 0.3; // Assign a low default score
    }

    // Return average confidence as a value between 0.0 and 1.0
    return totalConfidence / textBlockCount / 100;
  }
}

Categories

  • API (REST)
  • Analytics
  • API (GraphQL)
  • Auth
  • Authenticator
  • DataStore
  • Notifications (Push)
  • Storage

Steps to Reproduce

Here's a detailed "Steps to Reproduce" section you can add to your GitHub issue:

### Steps to Reproduce

1. Create a new Flutter web project or use an existing one
2. Add the following dependencies to your pubspec.yaml:
   ```yaml
   dependencies:
    aws_signature_v4: ^0.6.3
    aws_common: ^0.7.5
    uuid: ^4.5.1
    crypto: ^3.0.6
    dotenv: ^4.2.0
    esc_pos_utils_plus: ^2.0.4
  1. Create a service class for AWS Textract (as shown in code example)

  2. Set up a simple UI with:

    • A button to select/capture a document image
    • A button to analyze the document using Textract
    • A text area to display results or errors
  3. Implement the document analysis workflow:

    // In your widget's event handler
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.gallery);
    
    if (image != null) {
      final bytes = await image.readAsBytes();
      
      try {
        final service = AwsTextractService.getInstance(
          accessKey: 'YOUR_ACCESS_KEY',
          secretKey: 'YOUR_SECRET_KEY',
          region: 'ap-south-1',
        );
        
        final clarity = await service.analyzeDocumentClarity(bytes);
        setState(() {
          resultText = "Document clarity score: ${clarity.toStringAsFixed(2)}";
        });
      } catch (e) {
        setState(() {
          resultText = "Error: $e";
        });
        print('Full error: $e');
      }
    }
  4. Select a document image and click the analyze button

  5. Observe in the Network tab:

    • An OPTIONS request to the Textract endpoint failing with a 404 status
    • The subsequent POST request failing with a CORS error
  6. Check the Console tab for the complete error message about CORS policy violation



### Screenshots

<img width="1088" alt="Image" src="https://github.com/user-attachments/assets/e1738002-fa29-4dbf-902d-8c17dc0dbb9b" />

<img width="1081" alt="Image" src="https://github.com/user-attachments/assets/10aa6a7c-098a-4370-8eb4-9d359fc09663" />

### Platforms

- [ ] iOS
- [ ] Android
- [x] Web
- [ ] macOS
- [ ] Windows
- [ ] Linux

### Flutter Version

3.29.0

### Amplify Flutter Version

aws_common: ^0.7.5

### Deployment Method

Amplify Gen 2

### Schema

```GraphQL

@github-actions github-actions bot added pending-triage This issue is in the backlog of issues to triage pending-maintainer-response Pending response from a maintainer of this repository labels Mar 1, 2025
@WrathOP
Copy link
Author

WrathOP commented Mar 1, 2025

There's this version I tried also where I was using HTTP.post after using the AWS sdks

import 'dart:convert';
import 'dart:typed_data';
import 'package:aws_textract_api/textract-2018-06-27.dart';
import 'package:aws_common/aws_common.dart';
import 'package:aws_signature_v4/aws_signature_v4.dart';
import 'package:crypto/crypto.dart';
import 'package:esc_pos_utils_plus/dart_hex/hex.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';

/// AWS Textract service that uses the aws_signature_v4 package for authentication
class AwsTextractService {
  static AwsTextractService? _instance;

  final String accessKey;
  final String secretKey;
  final String region;
  final AWSCredentials _credentials;
  final AWSCredentialsProvider _credentialsProvider;
  late final AWSSigV4Signer _signer;
  final Uuid _uuid = const Uuid();

  // Private constructor for singleton
  AwsTextractService._({
    required this.accessKey,
    required this.secretKey,
    required this.region,
  })  : _credentials = AWSCredentials(accessKey, secretKey),
        _credentialsProvider =
            AWSCredentialsProvider(AWSCredentials(accessKey, secretKey)) {
    _signer = AWSSigV4Signer(
      credentialsProvider: _credentialsProvider,
    );
  }

  /// Get the singleton instance
  static AwsTextractService getInstance({
    required String accessKey,
    required String secretKey,
    String region = 'ap-south-1',
  }) {
    _instance ??= AwsTextractService._(
      accessKey: accessKey,
      secretKey: secretKey,
      region: region,
    );
    return _instance!;
  }

  String _formatDate(DateTime dateTime) {
    return '${dateTime.year}${dateTime.month.toString().padLeft(2, '0')}${dateTime.day.toString().padLeft(2, '0')}';
  }

  String _formatAmzDate(DateTime dateTime) {
    return '${_formatDate(dateTime)}T${dateTime.hour.toString().padLeft(2, '0')}${dateTime.minute.toString().padLeft(2, '0')}${dateTime.second.toString().padLeft(2, '0')}Z';
  }

  /// Analyze document clarity using AWS Textract
  /// Returns a confidence score between 0.0 and 1.0
  Future<double> analyzeDocumentClarity(Uint8List documentBytes) async {
    try {
      // Host and endpoint information
      final host = 'textract.$region.amazonaws.com';
      final endpoint = Uri.parse('https://$host');
      final timestamp = DateTime.now().toUtc();
      final amzDate = _formatAmzDate(timestamp);

      // Prepare the request body
      final requestBody = jsonEncode({
        'Document': {
          'Bytes': base64Encode(documentBytes),
        },
      });

      // Create a unique request ID
      final requestId = _uuid.v4();

      // Prepare headers
      final headers = {
        'Content-Type': 'application/x-amz-json-1.1',
        'X-Amz-Target': 'Textract.DetectDocumentText',
        'amz-sdk-invocation-id': requestId,
        'amz-sdk-request': 'attempt=1; max=3',
        'Accept': '*/*',
        'X-Amz-Date': amzDate,
        'X-Amz-Content-Sha256': const HexEncoder()
            .convert(sha256.convert(utf8.encode(requestBody)).bytes),
      };

      debugPrint('Headers: $headers');

      // Create the AWS request
      final request = AWSHttpRequest(
        method: AWSHttpMethod.post,
        uri: endpoint,
        headers: headers,
        body: Uint8List.fromList(utf8.encode(requestBody)),
      );

      final scope = AWSCredentialScope(
        region: 'ap-south-1',
        service: AWSService.textract,
      );

      debugPrint('Request: $request');

      // Sign the request
      final AWSSignedRequest signedRequest = await _signer.sign(
        request,
        credentialScope: scope,
        // this is un-nessecarry
        serviceConfiguration: const ServiceConfiguration(
          signBody: true,
          normalizePath: true,
          omitSessionToken: true,
          doubleEncodePathSegments: true,
        ),
      );

      debugPrint(
          'Signed Headers: ${Map<String, String>.from(signedRequest.headers)}');

      // Make the HTTP request
      final response = await http.post(
        endpoint,
        headers: Map<String, String>.from(signedRequest.headers),
        body: requestBody,
      );

      if (response.statusCode != 200) {
        throw Exception(
          'AWS Textract request failed with status ${response.statusCode}: ${response.body}',
        );
      }

      // Parse the response
      final responseJson = jsonDecode(response.body);
      final textractResponse =
          DetectDocumentTextResponse.fromJson(responseJson);

      // Calculate and return the document clarity score
      return _calculateDocumentClarityScore(textractResponse);
    } catch (e) {
      print('Error analyzing document: $e');
      rethrow;
    }
  }

  /// Calculate document clarity score based on Textract response
  double _calculateDocumentClarityScore(DetectDocumentTextResponse response) {
    final blocks = response.blocks;

    if (blocks == null || blocks.isEmpty) {
      return 0.0; // No text detected
    }

    // Calculate average confidence across all detected text blocks
    double totalConfidence = 0.0;
    int textBlockCount = 0;

    for (final block in blocks) {
      if (block.blockType == BlockType.line ||
          block.blockType == BlockType.word) {
        if (block.confidence != null) {
          totalConfidence += block.confidence!;
          textBlockCount++;
        }
      }
    }

    // If no text blocks found, document might be blank or an image
    if (textBlockCount == 0) {
      return 0.3; // Assign a low default score
    }

    // Return average confidence as a value between 0.0 and 1.0
    return totalConfidence / textBlockCount / 100;
  }
}

Also these are the headers that are being generated in this:

Signed Headers: {Content-Type: application/x-amz-json-1.1, X-Amz-Target: Textract.DetectDocumentText, amz-sdk-invocation-id: f451edd0-421c-40fc-94da-f7bc2bf8f09c, amz-sdk-request:
attempt=1; max=3, Accept: */*, X-Amz-Date: 20250301T122905Z, X-Amz-Content-Sha256: cec9599bbd1577619a459aaea121d8f7a391c0df326671f32cb03878146f1850, X-Amz-User-Agent: aws-sigv4-dart/0.6.3,
Authorization: AWS4-HMAC-SHA256 Credential=AKIAVY2PHDTI7UWYWDHQ/20250301/ap-south-1/textract/aws4_request,
SignedHeaders=accept;amz-sdk-invocation-id;amz-sdk-request;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-target;x-amz-user-agent,
Signature=45143ae3b56cdb4a2491dfa7915e2a0d38a0f8db72f601cb896254ff6687b546}

I was getting response:

{"__type":"InvalidSignatureException","message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}

I would say this is the main issue why is my signature not valid what am I doing wrong here. BTW I am 100% on my creds being right as I tried the same creds using the react sdk.

@WrathOP WrathOP changed the title Issue: AWS Textract Service - 404 in OPTIONS request and CORS errors Issue: AWS Textract Service - 400 and CORS errors Mar 1, 2025
@WrathOP
Copy link
Author

WrathOP commented Mar 1, 2025

Why does sending the request using the AWSSignedRequest.send() add a /? at the end there are no query params this results in 404 error

curl 'https://textract.ap-south-1.amazonaws.com/?'
-X 'OPTIONS'
-H 'Accept: /'
-H 'Accept-Language: en-US,en;q=0.9'
-H 'Access-Control-Request-Headers: amz-sdk-invocation-id,amz-sdk-request,authorization,content-type,x-amz-content-sha256,x-amz-date,x-amz-target,x-amz-user-agent'
-H 'Access-Control-Request-Method: POST'
-H 'Connection: keep-alive'
-H 'Origin: http://localhost:49536'
-H 'Referer: http://localhost:49536/'
-H 'Sec-Fetch-Dest: empty'
-H 'Sec-Fetch-Mode: cors'
-H 'Sec-Fetch-Site: cross-site'
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'

This is what it's sending. If I remove the /? from the uri it results in 200.

Anyway the original post request doesn't work either way. It always errors:

"__type": "InvalidSignatureException",
"message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pending-maintainer-response Pending response from a maintainer of this repository pending-triage This issue is in the backlog of issues to triage
Projects
None yet
Development

No branches or pull requests

1 participant