[Bug]: NullPointerException in A2ACommonFieldMapper when a struct field has an explicit null value

Đang mở Phù hợp với người mới
#1,160 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
78/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
java
Lĩnh vực
api

Hướng nghiên cứu

Bắt đầu trong A2ACommonFieldMapper.structToMap và valueToObject, sau đó lần theo đường dẫn TaskMapperImpl/MessageMapperImpl được Client.getTask(...) gọi tới qua JSON-RPC. Hoàn tất khi một phản hồi chứa các trường null tường minh lồng nhau được chuyển đổi mà không xảy ra NullPointerException và các trường null vẫn được biểu diễn trong dữ liệu kết quả.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

What happened?

A2ACommonFieldMapper.structToMap throws a NullPointerException when converting a protobuf Struct that contains a field whose value is an explicit JSON null (protobuf Value.KindCase.NULL_VALUE). This reproduces on 1.3.0.Final when deserializing a Task returned from GetTask, specifically when a message part's structured data contains a null field anywhere in its (possibly nested) object.

This looks like an incomplete fix of #618: that issue fixed the empty-struct case (valueToObject now short-circuits STRUCT_VALUE with 0 fields to Collections.emptyMap() — see lines 218-224 in the current source), but the explicit-null-field case was not addressed and still NPEs.

Root Cause

valueToObject correctly returns Java null for a protobuf NULL_VALUE field:

  case NULL_VALUE:                                                                                                                                                                                                                                                            
  default:                                                                                                                                                                                                                                                                    
      return null;                                                                                                                                                                                                                                                            

But structToMap collects the struct's fields with Collectors.toMap:

  default Map<String, Object> structToMap(Struct struct) {                                                                                                                                                                                                                    
      if (struct == null || struct.getFieldsCount() == 0) {                                                                                                                                                                                                                   
          return null;                                                                                                                                                                                                                                                        
      }                                                                                                                                                                                                                                                                       
      return struct.getFieldsMap().entrySet().stream()                                                                                                                                                                                                                        
              .collect(Collectors.toMap(Map.Entry::getKey, e -> valueToObject(e.getValue())));                                                                                                                                                                                
  }

Collectors.toMap's default accumulator calls Map.merge, which calls Objects.requireNonNull(value) — it cannot accept a null mapped value, regardless of how deeply nested the offending field is. Any JSON object with a null field anywhere in a message's structured data part therefore breaks task/message deserialization entirely.

Steps to Reproduce

  1. Have a server return (via GetTask, or any response going through TaskMapperImpl/MessageMapperImpl) a message part whose structured JSON data contains an explicit null field, e.g.:
  {                                                                                                                                                                                                                                                                           
    "data": {                                                                                                                                                                                                                                                                 
      "data": {                                                                                                                                                                                                                                                               
        "some_optional_field": null,                                                                                                                                                                                                                                          
        "other_field": "value"                                                                                                                                                                                                                                                
      }                                                                                                                                                                                                                                                                       
    }                                                                                                                                                                                                                                                                         
  }
  1. Have the client call Client.getTask(...) (JSON-RPC transport) to fetch that task.
  2. A NullPointerException is thrown while converting the response, and the client never gets to see the actual task/message content.
Relevant log output
java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
	at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:229)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
	at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
	at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.lambda$structToMap$1(A2ACommonFieldMapper.java:170)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
	at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet.lambda$entryConsumer$0(Collections.java:1778)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
	at java.base/java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntrySetSpliterator.forEachRemaining(Collections.java:1803)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.structToMap(A2ACommonFieldMapper.java:170)
	at org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.valueToObject(A2ACommonFieldMapper.java:225)
	at org.a2aproject.sdk.grpc.mapper.PartMapper.fromProto(PartMapper.java:110)
	at org.a2aproject.sdk.grpc.mapper.MessageMapperImpl.partListToPartArray(MessageMapperImpl.java:88)
	at org.a2aproject.sdk.grpc.mapper.MessageMapperImpl.fromProto(MessageMapperImpl.java:72)
	at org.a2aproject.sdk.grpc.mapper.TaskMapperImpl.messageListToMessageArray(TaskMapperImpl.java:95)
	at org.a2aproject.sdk.grpc.mapper.TaskMapperImpl.fromProto(TaskMapperImpl.java:69)
	at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.lambda$task$11(ProtoUtils.java:313)
	at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.convert(ProtoUtils.java:198)
	at org.a2aproject.sdk.grpc.utils.ProtoUtils$FromProto.task(ProtoUtils.java:313)
	at org.a2aproject.sdk.grpc.utils.JSONRPCUtils.parseResponseBody(JSONRPCUtils.java:315)
	at org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport.unmarshalResponse(JSONRPCTransport.java:369)
	at org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport.getTask(JSONRPCTransport.java:148)
	at org.a2aproject.sdk.client.Client.getTask(Client.java:371)
(application code)
Code of Conduct
  • I agree to follow this project's Code of Conduct
Ngôn ngữ chính
Java
Star
495
Fork
174
Merge trung bình
1 ngày 18 giờ
Pull request đã merge (30 ngày)
48

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của a2aproject/a2a-java

Tất cả issue của a2aproject/a2a-java

Issue tương tự

Thêm issue về Java

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.