ESP8266 Panic core_esp8266_main.cpp:191 __yield crash

Hello Arduino community, the program I'm trying to make is:
My ESP fetches my school's timetable:
-to do that, it needs to:
--log-in via login endpoint, extract the access_token
--send another http request to the timetable endpoint and extract json that gets sent back
An eink display displays the timetable for the week

Unfortunately I haven't yet been able to get the access_token extracting part working.
Here's how I'm trying to do it:

  1. receive the http response with the token in the body in json format
  2. check if response code is 200
  3. skip all characters until the json opening '{'
  4. find the 'access_header' key and stop right before it
  5. load the key into buffer until i hit a " < this is the error part

Here's my complete code for context. I tried to put comments to make it easier to understand. The most important part is the last loop of the getHeader() function

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

#define TIMEOUT 10000

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);
  Serial.println();
  Serial.println();

  initWifi(); // basic wifi connecting function

  JsonDocument timetable; // the json where ill eventually be saving my timetable, not important
  if(getTimetable(timetable))
    fail();

}

void loop() {
}


void initWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.setPhyMode(WIFI_PHY_MODE_11G);
  WiFi.begin(F("SSID"), F("PASSWORD"));

  int dots = 0;
  Serial.print(F("wifi "));
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(WiFi.status());
    Serial.flush();
    dots++;
    if (dots > 100)
      fail();
    visualDelay(500);
    yield();
  }
  Serial.println();
  Serial.println(F("done"));
}

int getTimetable(JsonDocument& timetable) {
  WiFiClientSecure client;
  client.setInsecure();
  client.setBufferSizes(512,512); // low buffer size to save memory, the server does support it

  if (client.connect(F("my-school-server"), 443)) {
    Serial.println(F("connected"));
  } else {
    Serial.println(F("fail"));
    return -2;
  }

  client.println(F("POST /api/login HTTP/1.1"));
  client.println(F("host: my-school-server"));
  client.println(F("accept: */*"));
  client.println(F("content-length: 73"));
  client.println(F("content-type: application/x-www-form-urlencoded"));
  client.println();
  client.print(F("body-that-contains-login-info"));

// just a simple loop that checks if data has arrived yet
  if(waitForData(client)) {
    Serial.println(F("timeout"));
    return -3;
  }

  // check for 200 and dont proceed otherwise
  if(failed(client)) {
    Serial.println(F("non 200 response code"));
    return -4;
  } else {
    Serial.println(F("200"));
  }


  char header[6000];
  getHeader(client, header);

client.abort();
  return 0;
}



void getHeader(WiFiClientSecure& client, char (&header)[6000]) {

  while (readw(client) != '{') // skip until the opening of the json file
    ;

  size_t i;
  int c;

  const char* tokstr = "\"access_token\": \""; // the json key i am searching for
  char buffer[20];
  while ((c = readw(client)) != -1) {
    for (i = 0; i < sizeof(buffer) - 2; i++)
      buffer[i] = buffer[i + 1];
    buffer[i++] = c;
    buffer[i] = '\0';
    if (strstr(buffer, tokstr)) // if found in the last 19 chars break
      break;
    yield();
  }

  Serial.println(ESP.getFreeHeap()); // prints over 20k
  int x;
// now just to test i fill the buffer 2 times to show there is no error and its not a memory problem
  for(x = 0; x < sizeof(header)-1; x++)
    header[x] = 'n';
  header[x] = '\0';
  Serial.println(header);
  for(x = 0; x < sizeof(header)-1; x++)
    header[x] = 'x';
  header[x] = '\0';
  Serial.println(header);

// now i need to construct the header, i first put the authorization part of the header into the buffer and then start loading the stream into it until i hit a '"' char that means the end of the json string.
// THIS IS WHERE THE ERROR IS
  const char* headerStart = "authorization: Bearer ";
  strcpy(header, headerStart);
  i = strlen(header);
  while((c = readw(client)) != -1 && c != '"' && i < sizeof(header) - 1) { 
    header[i++] = c;
    Serial.println(i);
    yield(); //yield due to watchdog crashing
  }
  header[i] = '\0';
  Serial.println(header);
}



int failed(WiFiClientSecure& client) {
  char buffer[20];
  const char* success = "200";
  int i = client.readBytesUntil('\n', buffer, sizeof(buffer) - 1);
  buffer[i] = '\0';
  if (!strstr(buffer, success)) {
    return 1;
  } else {
    return 0;
  }
}



int waitForData(WiFiClientSecure& client) {
  unsigned long t0 = millis();
  while (client.available() == 0) {
    if (millis() - t0 > TIMEOUT) {
      return 1;
    }
    yield();
  }
  return 0;
}


// read that tries twice in case some data hasnt arrived yet, i found its important due to the small buffer size
int readw(WiFiClientSecure& client) {
  int c;
  if((c = client.read()) != -1)
    return c;
  Serial.println(F("no data tryin again"));
  delay(100);
  yield();
  if((c = client.read()) != -1)
    return c;
  return -1;
}

void visualDelay(unsigned long duration) { // delay that also flashes the led
  digitalWrite(LED_BUILTIN, LOW);
  delay(duration / 2);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(duration / 2);
}

void fail() { // in case of error, just start flashing the led
  Serial.println(F("failed!"));
  for (;;) {
    visualDelay(3000);
  }
}

Here's the exception:

20648 // over 20k of free heap, thats no problem
nnnnnnnnn...
xxxxxxxxxxxxxxxxxx... // <== x and n print 6000 times, this is to show writing to the buffer is no problem
23 // <=== the first iteration of the loop after copying in the "authorization: bearer " part

User exception (panic/abort/assert)
--------------- CUT HERE FOR EXCEPTION DECODER ---------------

Panic core_esp8266_main.cpp:191 __yield

>>>stack>>>

ctx: sys
sp: 3fffe660 end: 3fffffb0 offset: 0010
3fffe670:  3ffffe98 00000002 3fff291c 40209149  
3fffe680:  3ffffe98 3fff291c 3fffe6e0 40201a83  
3fffe690:  61222020 73656363 6f745f73 226e656b  
3fffe6a0:  0022203a fffffffc 00000080 4020187a  
3fffe6b0:  00000017 3ffe8842 3fff291c 3fff291c  
3fffe6c0:  3fffff50 00000008 3fff291c 3fff291c  
3fffe6d0:  3fffff50 3ffffde0 3ffffe98 40203470  
3fffe6e0:  68747561 7a69726f 6f697461 42203a6e  
3fffe6f0:  65726165 78652072 78787878 78787878  
3fffe700:  78787878 78787878 78787878 78787878  
3fffe710:  78787878 78787878 78787878 78787878  
3fffe720:  78787878 78787878 78787878 78787878  
3fffe730:  78787878 78787878 78787878 78787878  
3fffe740:  78787878 78787878 78787878 78787878  
3fffe750:  78787878 78787878 78787878 78787878  
3fffe760:  78787878 78787878 78787878 78787878  
3fffe770:  78787878 78787878 78787878 78787878  
3fffe780:  78787878 78787878 78787878 78787878  
3fffe790:  78787878 78787878 78787878 78787878  

... repeats, the new ide makes it very hard to copy long stack traces, sorry

3ffffde0:  78787878 78787878 78787878 78787878  
3ffffdf0:  78787878 78787878 78787878 78787878  
3ffffe00:  78787878 78787878 78787878 78787878  
3ffffe10:  78787878 78787878 78787878 78787878  
3ffffe20:  78787878 78787878 78787878 78787878  
3ffffe30:  78787878 78787878 78787878 78787878  
3ffffe40:  78787878 78787878 78787878 00787878  
3ffffe50:  00000000 00000000 00000000 4020a0ec  
3ffffe60:  3ffffe94 fffffffc 00000000 4020a0ec  
3ffffe70:  3ffffec0 fffffffc 00000064 3fff291c  
3ffffe80:  00000004 4025ae24 00000000 4020830c  
3ffffe90:  656e6f64 0019a2a7 4020caac 00000000  
3ffffea0:  00001388 00001a28 00000000 00000000  
3ffffeb0:  00000000 3fff38d4 3fff38d4 3fff3b4c  
3ffffec0:  00001429 3fff2a8c 000000fa 000000fa  
3ffffed0:  0000005c 000000fa 00001429 40209209  
3ffffee0:  402529ce 3fff291c 3fff28e0 4020a0ec  
3ffffef0:  3ffe8844 fffffffc 3ffe85f4 3fff28e0  
3fffff00:  40207fcc 3ffe8842 3fff291c 4020a0ec  
3fffff10:  3ffe8844 fffffffc 00000080 40203b2a  
3fffff20:  00000001 3ffe8842 3fff291c 3fff2ab4  
3fffff30:  3fffdad0 00000004 3fff291c 3fff2ab4  
3fffff40:  3fffdad0 00000000 3fff291c 40203b9d  
3fffff50:  3ffe85e0 feefef00 00000000 feefeffe  
3fffff60:  feefeffe feefeffe feefeffe feefeffe  
3fffff70:  feefeffe feefeffe feefeffe 3fffff5c  
3fffff80:  00040000 feefffff feefeffe feefeffe  
3fffff90:  feefef00 feefeffe feefeffe feefeffe  
3fffffa0:  feefeffe feefeffe feefeffe feefeffe  
<<<stack<<<

--------------- CUT HERE FOR EXCEPTION DECODER ---------------

 ets Jan  8 2013,rst cause:4, boot mode:(3,6)

wdt reset
load 0x4010f000, len 3424, room 16 
tail 0
chksum 0x2e
load 0x3fff20b8, len 40, room 8 
tail 0
chksum 0x2b

Seems to be something wrong with the yield function, although I'm not sure what. Before it used to throw a watchdog error, and sometimes it also threw an exception 9 error, which would mean LoadStoreAlignmentCause.
I'll try to explain the error part in more detail

  const char* headerStart = "authorization: Bearer "; // When I'll send the token, I'll need it to be in the HTTP header format
  strcpy(header, headerStart); // copy over the beginning of the header
  i = strlen(header); // get the index where the beginning part ends
  while((c = readw(client)) != -1 && i < sizeof(header) - 1) { 
    if(c == '"') // if " is encountered stop copying, the token has ended
      break;
    header[i++] = c;
    Serial.println(i);
    yield(); //< line causing the crash!
  }
  header[i] = '\0'; 
  Serial.println(header); // not once reached this part :(

I'd be very thankful for anyone willing to take a look at my code.

EDIT: Ran it through an exception decoder, the line causing the hangup is the yield line. But without it, watchdog crashes the program. What do I do?

try
while((c == readw(client))

You don't provide complete information, and with c-strings in particular that is tricky.

1.5 seconds ? really ?

I understand that you want to assign c the value of readw(client) but i think you should do this in a separate line. Particularly since we don't see that function it is a little hard to tell, but the assignment should always return 'true' and i guess

i = strlen(header); // get the index where the beginning part ends
  while((c = readw(client)) != -1 && i < sizeof(header) - 1) { 

may be better of as

i = strlen(header); // get the index where the beginning part ends
  while((c = readw(client)) != -1 && i < strlen(header) - 1) { 

Hi, thanks for the response, you can see the readw() function in the code:

// read that tries twice in case some data hasnt arrived yet, i found its important due to the small buffer size
int readw(WiFiClientSecure& client) {
  int c;
  if((c = client.read()) != -1)
    return c;
  Serial.println(F("no data tryin again"));
  delay(100);
  yield();
  if((c = client.read()) != -1)
    return c;
  return -1;
}

It's just a simple wrapper for client.read(). client.read() returns a character when it's available in the stream and -1 when no character is available. My wrapper returns the same values, but when client.read() returns -1 it waits a bit and then tries client.read() again, in case new data has arrived. It's important in my case due to my really small client buffer size, which causes the stream to sometimes be exhausted before new data has arrived.

The assignment returns the value that was assigned, it's just a nice short way of assigning the value and checking if it doesn't equal -1.

  while((c = readw(client)) != -1 && c != '"' && i < sizeof(header) - 1) { 

I have moved the == '"' checking to the while condition to make it more clear what I'm doing.
I am trying to fill up the header buffer with the token characters, until either it has encountered a " character, there is no more data, or the buffer has reached its capacity.

There was a very sneaky reason for the crash. I was exceeding the stack size limit of 4kB. When I made header static it started working as expected.

I guess that is also the reason that the webserver library makes use of the String class extensively for it's local variables. Using the head is of course fine as long as you destroy all objects afterwards and don't expand any global objects. It is also rather wasteful to have 6000 bytes reserved for a variable that you actually only use locally.

btw would that mean that

inline int readw(WiFiClientSecure& client) {
  int c;
  if((c = client.read()) != -1)
    return c;
  Serial.println(F("no data tryin again"));
  delay(100);
  yield();
  if((c = client.read()) != -1)
    return c;
  return -1;
}

would not cause the core to panic, since an inline function doesn't push the variables (and program pointer) onto the stack ? or would some of the other calls already do this

Doesn't really matter i guess, but i really do not see the need to have a fixed size variable that size if it is just used locally, whereas if it is a global variable and used as such, a fixed size declaration is actually preferred to prevent fragmentation.

I didn't know about the 4KB stack (push ?) limit, so i learned something. !

I actually made it local on purpose, I wanted to save memory by having the buffer deallocate right after I didn't need it anymore. I'm still not quite sure about the exact reason it threw the yield panic crash, to me it makes no sense in this context since afaik the error means yield was called from some other context than setup or loop, which in not the case here. Only explanation I can think of is just undefined behaviour due to exceeding the stack size.

Well that can still be done of course, but not on the stack but on the heap.
It is totally fine to do what you did using the String class and the size will actually be dynamic and fully destroyed afterwards. The code will also look cleaner. I personally got a little confused because i never use c-strings for these kind of things on an ESP.

At some point i guess it doesn't matter, Its the moment that the stack-pointer starts pointing to something which doesn't exists or well anyway not the proper spot anymore, or the program pointer gets incorrect information returned from the stack.